validators.py 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208
  1. """Various low level data validators."""
  2. from __future__ import annotations
  3. import calendar
  4. from collections.abc import Mapping, Sequence
  5. from io import open
  6. import fontTools.misc.filesystem as fs
  7. from typing import Any, Type, Optional, Union
  8. from fontTools.annotations import IntFloat
  9. from fontTools.ufoLib.utils import numberTypes
  10. GenericDict = dict[str, tuple[Union[type, tuple[Type[Any], ...]], bool]]
  11. # -------
  12. # Generic
  13. # -------
  14. def isDictEnough(value: Any) -> bool:
  15. """
  16. Some objects will likely come in that aren't
  17. dicts but are dict-ish enough.
  18. """
  19. if isinstance(value, Mapping):
  20. return True
  21. for attr in ("keys", "values", "items"):
  22. if not hasattr(value, attr):
  23. return False
  24. return True
  25. def genericTypeValidator(value: Any, typ: Type[Any]) -> bool:
  26. """
  27. Generic. (Added at version 2.)
  28. """
  29. return isinstance(value, typ)
  30. def genericIntListValidator(values: Any, validValues: Sequence[int]) -> bool:
  31. """
  32. Generic. (Added at version 2.)
  33. """
  34. if not isinstance(values, (list, tuple)):
  35. return False
  36. valuesSet = set(values)
  37. validValuesSet = set(validValues)
  38. if valuesSet - validValuesSet:
  39. return False
  40. for value in values:
  41. if not isinstance(value, int):
  42. return False
  43. return True
  44. def genericNonNegativeIntValidator(value: Any) -> bool:
  45. """
  46. Generic. (Added at version 3.)
  47. """
  48. if not isinstance(value, int):
  49. return False
  50. if value < 0:
  51. return False
  52. return True
  53. def genericNonNegativeNumberValidator(value: Any) -> bool:
  54. """
  55. Generic. (Added at version 3.)
  56. """
  57. if not isinstance(value, numberTypes):
  58. return False
  59. if value < 0:
  60. return False
  61. return True
  62. def genericDictValidator(value: Any, prototype: GenericDict) -> bool:
  63. """
  64. Generic. (Added at version 3.)
  65. """
  66. # not a dict
  67. if not isinstance(value, Mapping):
  68. return False
  69. # missing required keys
  70. for key, (typ, required) in prototype.items():
  71. if not required:
  72. continue
  73. if key not in value:
  74. return False
  75. # unknown keys
  76. for key in value.keys():
  77. if key not in prototype:
  78. return False
  79. # incorrect types
  80. for key, v in value.items():
  81. prototypeType, required = prototype[key]
  82. if v is None and not required:
  83. continue
  84. if not isinstance(v, prototypeType):
  85. return False
  86. return True
  87. # --------------
  88. # fontinfo.plist
  89. # --------------
  90. # Data Validators
  91. def fontInfoStyleMapStyleNameValidator(value: Any) -> bool:
  92. """
  93. Version 2+.
  94. """
  95. options = ["regular", "italic", "bold", "bold italic"]
  96. return value in options
  97. def fontInfoOpenTypeGaspRangeRecordsValidator(value: Any) -> bool:
  98. """
  99. Version 3+.
  100. """
  101. if not isinstance(value, list):
  102. return False
  103. if len(value) == 0:
  104. return True
  105. validBehaviors = [0, 1, 2, 3]
  106. dictPrototype: GenericDict = dict(
  107. rangeMaxPPEM=(int, True), rangeGaspBehavior=(list, True)
  108. )
  109. ppemOrder = []
  110. for rangeRecord in value:
  111. if not genericDictValidator(rangeRecord, dictPrototype):
  112. return False
  113. ppem = rangeRecord["rangeMaxPPEM"]
  114. behavior = rangeRecord["rangeGaspBehavior"]
  115. ppemValidity = genericNonNegativeIntValidator(ppem)
  116. if not ppemValidity:
  117. return False
  118. behaviorValidity = genericIntListValidator(behavior, validBehaviors)
  119. if not behaviorValidity:
  120. return False
  121. ppemOrder.append(ppem)
  122. if ppemOrder != sorted(ppemOrder):
  123. return False
  124. return True
  125. def fontInfoOpenTypeHeadCreatedValidator(value: Any) -> bool:
  126. """
  127. Version 2+.
  128. """
  129. # format: 0000/00/00 00:00:00
  130. if not isinstance(value, str):
  131. return False
  132. # basic formatting
  133. if not len(value) == 19:
  134. return False
  135. if value.count(" ") != 1:
  136. return False
  137. strDate, strTime = value.split(" ")
  138. if strDate.count("/") != 2:
  139. return False
  140. if strTime.count(":") != 2:
  141. return False
  142. # date
  143. strYear, strMonth, strDay = strDate.split("/")
  144. if len(strYear) != 4:
  145. return False
  146. if len(strMonth) != 2:
  147. return False
  148. if len(strDay) != 2:
  149. return False
  150. try:
  151. intYear = int(strYear)
  152. intMonth = int(strMonth)
  153. intDay = int(strDay)
  154. except ValueError:
  155. return False
  156. if intMonth < 1 or intMonth > 12:
  157. return False
  158. monthMaxDay = calendar.monthrange(intYear, intMonth)[1]
  159. if intDay < 1 or intDay > monthMaxDay:
  160. return False
  161. # time
  162. strHour, strMinute, strSecond = strTime.split(":")
  163. if len(strHour) != 2:
  164. return False
  165. if len(strMinute) != 2:
  166. return False
  167. if len(strSecond) != 2:
  168. return False
  169. try:
  170. intHour = int(strHour)
  171. intMinute = int(strMinute)
  172. intSecond = int(strSecond)
  173. except ValueError:
  174. return False
  175. if intHour < 0 or intHour > 23:
  176. return False
  177. if intMinute < 0 or intMinute > 59:
  178. return False
  179. if intSecond < 0 or intSecond > 59:
  180. return False
  181. # fallback
  182. return True
  183. def fontInfoOpenTypeNameRecordsValidator(value: Any) -> bool:
  184. """
  185. Version 3+.
  186. """
  187. if not isinstance(value, list):
  188. return False
  189. dictPrototype: GenericDict = dict(
  190. nameID=(int, True),
  191. platformID=(int, True),
  192. encodingID=(int, True),
  193. languageID=(int, True),
  194. string=(str, True),
  195. )
  196. for nameRecord in value:
  197. if not genericDictValidator(nameRecord, dictPrototype):
  198. return False
  199. return True
  200. def fontInfoOpenTypeOS2WeightClassValidator(value: Any) -> bool:
  201. """
  202. Version 2+.
  203. """
  204. if not isinstance(value, int):
  205. return False
  206. if value < 0:
  207. return False
  208. return True
  209. def fontInfoOpenTypeOS2WidthClassValidator(value: Any) -> bool:
  210. """
  211. Version 2+.
  212. """
  213. if not isinstance(value, int):
  214. return False
  215. if value < 1:
  216. return False
  217. if value > 9:
  218. return False
  219. return True
  220. def fontInfoVersion2OpenTypeOS2PanoseValidator(values: Any) -> bool:
  221. """
  222. Version 2.
  223. """
  224. if not isinstance(values, (list, tuple)):
  225. return False
  226. if len(values) != 10:
  227. return False
  228. for value in values:
  229. if not isinstance(value, int):
  230. return False
  231. # XXX further validation?
  232. return True
  233. def fontInfoVersion3OpenTypeOS2PanoseValidator(values: Any) -> bool:
  234. """
  235. Version 3+.
  236. """
  237. if not isinstance(values, (list, tuple)):
  238. return False
  239. if len(values) != 10:
  240. return False
  241. for value in values:
  242. if not isinstance(value, int):
  243. return False
  244. if value < 0:
  245. return False
  246. # XXX further validation?
  247. return True
  248. def fontInfoOpenTypeOS2FamilyClassValidator(values: Any) -> bool:
  249. """
  250. Version 2+.
  251. """
  252. if not isinstance(values, (list, tuple)):
  253. return False
  254. if len(values) != 2:
  255. return False
  256. for value in values:
  257. if not isinstance(value, int):
  258. return False
  259. classID, subclassID = values
  260. if classID < 0 or classID > 14:
  261. return False
  262. if subclassID < 0 or subclassID > 15:
  263. return False
  264. return True
  265. def fontInfoPostscriptBluesValidator(values: Any) -> bool:
  266. """
  267. Version 2+.
  268. """
  269. if not isinstance(values, (list, tuple)):
  270. return False
  271. if len(values) > 14:
  272. return False
  273. if len(values) % 2:
  274. return False
  275. for value in values:
  276. if not isinstance(value, numberTypes):
  277. return False
  278. return True
  279. def fontInfoPostscriptOtherBluesValidator(values: Any) -> bool:
  280. """
  281. Version 2+.
  282. """
  283. if not isinstance(values, (list, tuple)):
  284. return False
  285. if len(values) > 10:
  286. return False
  287. if len(values) % 2:
  288. return False
  289. for value in values:
  290. if not isinstance(value, numberTypes):
  291. return False
  292. return True
  293. def fontInfoPostscriptStemsValidator(values: Any) -> bool:
  294. """
  295. Version 2+.
  296. """
  297. if not isinstance(values, (list, tuple)):
  298. return False
  299. if len(values) > 12:
  300. return False
  301. for value in values:
  302. if not isinstance(value, numberTypes):
  303. return False
  304. return True
  305. def fontInfoPostscriptWindowsCharacterSetValidator(value: Any) -> bool:
  306. """
  307. Version 2+.
  308. """
  309. validValues = list(range(1, 21))
  310. if value not in validValues:
  311. return False
  312. return True
  313. def fontInfoWOFFMetadataUniqueIDValidator(value: Any) -> bool:
  314. """
  315. Version 3+.
  316. """
  317. dictPrototype: GenericDict = dict(id=(str, True))
  318. if not genericDictValidator(value, dictPrototype):
  319. return False
  320. return True
  321. def fontInfoWOFFMetadataVendorValidator(value: Any) -> bool:
  322. """
  323. Version 3+.
  324. """
  325. dictPrototype: GenericDict = {
  326. "name": (str, True),
  327. "url": (str, False),
  328. "dir": (str, False),
  329. "class": (str, False),
  330. }
  331. if not genericDictValidator(value, dictPrototype):
  332. return False
  333. if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
  334. return False
  335. return True
  336. def fontInfoWOFFMetadataCreditsValidator(value: Any) -> bool:
  337. """
  338. Version 3+.
  339. """
  340. dictPrototype: GenericDict = dict(credits=(list, True))
  341. if not genericDictValidator(value, dictPrototype):
  342. return False
  343. if not len(value["credits"]):
  344. return False
  345. dictPrototype = {
  346. "name": (str, True),
  347. "url": (str, False),
  348. "role": (str, False),
  349. "dir": (str, False),
  350. "class": (str, False),
  351. }
  352. for credit in value["credits"]:
  353. if not genericDictValidator(credit, dictPrototype):
  354. return False
  355. if "dir" in credit and credit.get("dir") not in ("ltr", "rtl"):
  356. return False
  357. return True
  358. def fontInfoWOFFMetadataDescriptionValidator(value: Any) -> bool:
  359. """
  360. Version 3+.
  361. """
  362. dictPrototype: GenericDict = dict(url=(str, False), text=(list, True))
  363. if not genericDictValidator(value, dictPrototype):
  364. return False
  365. for text in value["text"]:
  366. if not fontInfoWOFFMetadataTextValue(text):
  367. return False
  368. return True
  369. def fontInfoWOFFMetadataLicenseValidator(value: Any) -> bool:
  370. """
  371. Version 3+.
  372. """
  373. dictPrototype: GenericDict = dict(
  374. url=(str, False), text=(list, False), id=(str, False)
  375. )
  376. if not genericDictValidator(value, dictPrototype):
  377. return False
  378. if "text" in value:
  379. for text in value["text"]:
  380. if not fontInfoWOFFMetadataTextValue(text):
  381. return False
  382. return True
  383. def fontInfoWOFFMetadataTrademarkValidator(value: Any) -> bool:
  384. """
  385. Version 3+.
  386. """
  387. dictPrototype: GenericDict = dict(text=(list, True))
  388. if not genericDictValidator(value, dictPrototype):
  389. return False
  390. for text in value["text"]:
  391. if not fontInfoWOFFMetadataTextValue(text):
  392. return False
  393. return True
  394. def fontInfoWOFFMetadataCopyrightValidator(value: Any) -> bool:
  395. """
  396. Version 3+.
  397. """
  398. dictPrototype: GenericDict = dict(text=(list, True))
  399. if not genericDictValidator(value, dictPrototype):
  400. return False
  401. for text in value["text"]:
  402. if not fontInfoWOFFMetadataTextValue(text):
  403. return False
  404. return True
  405. def fontInfoWOFFMetadataLicenseeValidator(value: Any) -> bool:
  406. """
  407. Version 3+.
  408. """
  409. dictPrototype: GenericDict = {
  410. "name": (str, True),
  411. "dir": (str, False),
  412. "class": (str, False),
  413. }
  414. if not genericDictValidator(value, dictPrototype):
  415. return False
  416. if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
  417. return False
  418. return True
  419. def fontInfoWOFFMetadataTextValue(value: Any) -> bool:
  420. """
  421. Version 3+.
  422. """
  423. dictPrototype: GenericDict = {
  424. "text": (str, True),
  425. "language": (str, False),
  426. "dir": (str, False),
  427. "class": (str, False),
  428. }
  429. if not genericDictValidator(value, dictPrototype):
  430. return False
  431. if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
  432. return False
  433. return True
  434. def fontInfoWOFFMetadataExtensionsValidator(value: Any) -> bool:
  435. """
  436. Version 3+.
  437. """
  438. if not isinstance(value, list):
  439. return False
  440. if not value:
  441. return False
  442. for extension in value:
  443. if not fontInfoWOFFMetadataExtensionValidator(extension):
  444. return False
  445. return True
  446. def fontInfoWOFFMetadataExtensionValidator(value: Any) -> bool:
  447. """
  448. Version 3+.
  449. """
  450. dictPrototype: GenericDict = dict(
  451. names=(list, False), items=(list, True), id=(str, False)
  452. )
  453. if not genericDictValidator(value, dictPrototype):
  454. return False
  455. if "names" in value:
  456. for name in value["names"]:
  457. if not fontInfoWOFFMetadataExtensionNameValidator(name):
  458. return False
  459. for item in value["items"]:
  460. if not fontInfoWOFFMetadataExtensionItemValidator(item):
  461. return False
  462. return True
  463. def fontInfoWOFFMetadataExtensionItemValidator(value: Any) -> bool:
  464. """
  465. Version 3+.
  466. """
  467. dictPrototype: GenericDict = dict(
  468. id=(str, False), names=(list, True), values=(list, True)
  469. )
  470. if not genericDictValidator(value, dictPrototype):
  471. return False
  472. for name in value["names"]:
  473. if not fontInfoWOFFMetadataExtensionNameValidator(name):
  474. return False
  475. for val in value["values"]:
  476. if not fontInfoWOFFMetadataExtensionValueValidator(val):
  477. return False
  478. return True
  479. def fontInfoWOFFMetadataExtensionNameValidator(value: Any) -> bool:
  480. """
  481. Version 3+.
  482. """
  483. dictPrototype: GenericDict = {
  484. "text": (str, True),
  485. "language": (str, False),
  486. "dir": (str, False),
  487. "class": (str, False),
  488. }
  489. if not genericDictValidator(value, dictPrototype):
  490. return False
  491. if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
  492. return False
  493. return True
  494. def fontInfoWOFFMetadataExtensionValueValidator(value: Any) -> bool:
  495. """
  496. Version 3+.
  497. """
  498. dictPrototype: GenericDict = {
  499. "text": (str, True),
  500. "language": (str, False),
  501. "dir": (str, False),
  502. "class": (str, False),
  503. }
  504. if not genericDictValidator(value, dictPrototype):
  505. return False
  506. if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
  507. return False
  508. return True
  509. # ----------
  510. # Guidelines
  511. # ----------
  512. def guidelinesValidator(value: Any, identifiers: Optional[set[str]] = None) -> bool:
  513. """
  514. Version 3+.
  515. """
  516. if not isinstance(value, list):
  517. return False
  518. if identifiers is None:
  519. identifiers = set()
  520. for guide in value:
  521. if not guidelineValidator(guide):
  522. return False
  523. identifier = guide.get("identifier")
  524. if identifier is not None:
  525. if identifier in identifiers:
  526. return False
  527. identifiers.add(identifier)
  528. return True
  529. _guidelineDictPrototype: GenericDict = dict(
  530. x=((int, float), False),
  531. y=((int, float), False),
  532. angle=((int, float), False),
  533. name=(str, False),
  534. color=(str, False),
  535. identifier=(str, False),
  536. )
  537. def guidelineValidator(value: Any) -> bool:
  538. """
  539. Version 3+.
  540. """
  541. if not genericDictValidator(value, _guidelineDictPrototype):
  542. return False
  543. x = value.get("x")
  544. y = value.get("y")
  545. angle = value.get("angle")
  546. # x or y must be present
  547. if x is None and y is None:
  548. return False
  549. # if x or y are None, angle must not be present
  550. if x is None or y is None:
  551. if angle is not None:
  552. return False
  553. # if x and y are defined, angle must be defined
  554. if x is not None and y is not None and angle is None:
  555. return False
  556. # angle must be between 0 and 360
  557. if angle is not None:
  558. if angle < 0:
  559. return False
  560. if angle > 360:
  561. return False
  562. # identifier must be 1 or more characters
  563. identifier = value.get("identifier")
  564. if identifier is not None and not identifierValidator(identifier):
  565. return False
  566. # color must follow the proper format
  567. color = value.get("color")
  568. if color is not None and not colorValidator(color):
  569. return False
  570. return True
  571. # -------
  572. # Anchors
  573. # -------
  574. def anchorsValidator(value: Any, identifiers: Optional[set[str]] = None) -> bool:
  575. """
  576. Version 3+.
  577. """
  578. if not isinstance(value, list):
  579. return False
  580. if identifiers is None:
  581. identifiers = set()
  582. for anchor in value:
  583. if not anchorValidator(anchor):
  584. return False
  585. identifier = anchor.get("identifier")
  586. if identifier is not None:
  587. if identifier in identifiers:
  588. return False
  589. identifiers.add(identifier)
  590. return True
  591. _anchorDictPrototype: GenericDict = dict(
  592. x=((int, float), False),
  593. y=((int, float), False),
  594. name=(str, False),
  595. color=(str, False),
  596. identifier=(str, False),
  597. )
  598. def anchorValidator(value: Any) -> bool:
  599. """
  600. Version 3+.
  601. """
  602. if not genericDictValidator(value, _anchorDictPrototype):
  603. return False
  604. x = value.get("x")
  605. y = value.get("y")
  606. # x and y must be present
  607. if x is None or y is None:
  608. return False
  609. # identifier must be 1 or more characters
  610. identifier = value.get("identifier")
  611. if identifier is not None and not identifierValidator(identifier):
  612. return False
  613. # color must follow the proper format
  614. color = value.get("color")
  615. if color is not None and not colorValidator(color):
  616. return False
  617. return True
  618. # ----------
  619. # Identifier
  620. # ----------
  621. def identifierValidator(value: Any) -> bool:
  622. """
  623. Version 3+.
  624. >>> identifierValidator("a")
  625. True
  626. >>> identifierValidator("")
  627. False
  628. >>> identifierValidator("a" * 101)
  629. False
  630. """
  631. validCharactersMin = 0x20
  632. validCharactersMax = 0x7E
  633. if not isinstance(value, str):
  634. return False
  635. if not value:
  636. return False
  637. if len(value) > 100:
  638. return False
  639. for c in value:
  640. i = ord(c)
  641. if i < validCharactersMin or i > validCharactersMax:
  642. return False
  643. return True
  644. # -----
  645. # Color
  646. # -----
  647. def colorValidator(value: Any) -> bool:
  648. """
  649. Version 3+.
  650. >>> colorValidator("0,0,0,0")
  651. True
  652. >>> colorValidator(".5,.5,.5,.5")
  653. True
  654. >>> colorValidator("0.5,0.5,0.5,0.5")
  655. True
  656. >>> colorValidator("1,1,1,1")
  657. True
  658. >>> colorValidator("2,0,0,0")
  659. False
  660. >>> colorValidator("0,2,0,0")
  661. False
  662. >>> colorValidator("0,0,2,0")
  663. False
  664. >>> colorValidator("0,0,0,2")
  665. False
  666. >>> colorValidator("1r,1,1,1")
  667. False
  668. >>> colorValidator("1,1g,1,1")
  669. False
  670. >>> colorValidator("1,1,1b,1")
  671. False
  672. >>> colorValidator("1,1,1,1a")
  673. False
  674. >>> colorValidator("1 1 1 1")
  675. False
  676. >>> colorValidator("1 1,1,1")
  677. False
  678. >>> colorValidator("1,1 1,1")
  679. False
  680. >>> colorValidator("1,1,1 1")
  681. False
  682. >>> colorValidator("1, 1, 1, 1")
  683. True
  684. """
  685. if not isinstance(value, str):
  686. return False
  687. parts = value.split(",")
  688. if len(parts) != 4:
  689. return False
  690. for part in parts:
  691. part = part.strip()
  692. converted = False
  693. number: IntFloat
  694. try:
  695. number = int(part)
  696. converted = True
  697. except ValueError:
  698. pass
  699. if not converted:
  700. try:
  701. number = float(part)
  702. converted = True
  703. except ValueError:
  704. pass
  705. if not converted:
  706. return False
  707. if not 0 <= number <= 1:
  708. return False
  709. return True
  710. # -----
  711. # image
  712. # -----
  713. pngSignature: bytes = b"\x89PNG\r\n\x1a\n"
  714. _imageDictPrototype: GenericDict = dict(
  715. fileName=(str, True),
  716. xScale=((int, float), False),
  717. xyScale=((int, float), False),
  718. yxScale=((int, float), False),
  719. yScale=((int, float), False),
  720. xOffset=((int, float), False),
  721. yOffset=((int, float), False),
  722. color=(str, False),
  723. )
  724. def imageValidator(value):
  725. """
  726. Version 3+.
  727. """
  728. if not genericDictValidator(value, _imageDictPrototype):
  729. return False
  730. # fileName must be one or more characters
  731. if not value["fileName"]:
  732. return False
  733. # color must follow the proper format
  734. color = value.get("color")
  735. if color is not None and not colorValidator(color):
  736. return False
  737. return True
  738. def pngValidator(
  739. path: Optional[str] = None,
  740. data: Optional[bytes] = None,
  741. fileObj: Optional[Any] = None,
  742. ) -> tuple[bool, Any]:
  743. """
  744. Version 3+.
  745. This checks the signature of the image data.
  746. """
  747. assert path is not None or data is not None or fileObj is not None
  748. if path is not None:
  749. with open(path, "rb") as f:
  750. signature = f.read(8)
  751. elif data is not None:
  752. signature = data[:8]
  753. elif fileObj is not None:
  754. pos = fileObj.tell()
  755. signature = fileObj.read(8)
  756. fileObj.seek(pos)
  757. if signature != pngSignature:
  758. return False, "Image does not begin with the PNG signature."
  759. return True, None
  760. # -------------------
  761. # layercontents.plist
  762. # -------------------
  763. def layerContentsValidator(
  764. value: Any, ufoPathOrFileSystem: Union[str, fs.base.FS]
  765. ) -> tuple[bool, Optional[str]]:
  766. """
  767. Check the validity of layercontents.plist.
  768. Version 3+.
  769. """
  770. if isinstance(ufoPathOrFileSystem, fs.base.FS):
  771. fileSystem = ufoPathOrFileSystem
  772. else:
  773. fileSystem = fs.osfs.OSFS(ufoPathOrFileSystem)
  774. bogusFileMessage = "layercontents.plist in not in the correct format."
  775. # file isn't in the right format
  776. if not isinstance(value, list):
  777. return False, bogusFileMessage
  778. # work through each entry
  779. usedLayerNames = set()
  780. usedDirectories = set()
  781. contents = {}
  782. for entry in value:
  783. # layer entry in the incorrect format
  784. if not isinstance(entry, list):
  785. return False, bogusFileMessage
  786. if not len(entry) == 2:
  787. return False, bogusFileMessage
  788. for i in entry:
  789. if not isinstance(i, str):
  790. return False, bogusFileMessage
  791. layerName, directoryName = entry
  792. # check directory naming
  793. if directoryName != "glyphs":
  794. if not directoryName.startswith("glyphs."):
  795. return (
  796. False,
  797. "Invalid directory name (%s) in layercontents.plist."
  798. % directoryName,
  799. )
  800. if len(layerName) == 0:
  801. return False, "Empty layer name in layercontents.plist."
  802. # directory doesn't exist
  803. if not fileSystem.exists(directoryName):
  804. return False, "A glyphset does not exist at %s." % directoryName
  805. # default layer name
  806. if layerName == "public.default" and directoryName != "glyphs":
  807. return (
  808. False,
  809. "The name public.default is being used by a layer that is not the default.",
  810. )
  811. # check usage
  812. if layerName in usedLayerNames:
  813. return (
  814. False,
  815. "The layer name %s is used by more than one layer." % layerName,
  816. )
  817. usedLayerNames.add(layerName)
  818. if directoryName in usedDirectories:
  819. return (
  820. False,
  821. "The directory %s is used by more than one layer." % directoryName,
  822. )
  823. usedDirectories.add(directoryName)
  824. # store
  825. contents[layerName] = directoryName
  826. # missing default layer
  827. foundDefault = "glyphs" in contents.values()
  828. if not foundDefault:
  829. return False, "The required default glyph set is not in the UFO."
  830. return True, None
  831. # ------------
  832. # groups.plist
  833. # ------------
  834. def groupsValidator(value: Any) -> tuple[bool, Optional[str]]:
  835. """
  836. Check the validity of the groups.
  837. Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
  838. >>> groups = {"A" : ["A", "A"], "A2" : ["A"]}
  839. >>> groupsValidator(groups)
  840. (True, None)
  841. >>> groups = {"" : ["A"]}
  842. >>> valid, msg = groupsValidator(groups)
  843. >>> valid
  844. False
  845. >>> print(msg)
  846. A group has an empty name.
  847. >>> groups = {"public.awesome" : ["A"]}
  848. >>> groupsValidator(groups)
  849. (True, None)
  850. >>> groups = {"public.kern1." : ["A"]}
  851. >>> valid, msg = groupsValidator(groups)
  852. >>> valid
  853. False
  854. >>> print(msg)
  855. The group data contains a kerning group with an incomplete name.
  856. >>> groups = {"public.kern2." : ["A"]}
  857. >>> valid, msg = groupsValidator(groups)
  858. >>> valid
  859. False
  860. >>> print(msg)
  861. The group data contains a kerning group with an incomplete name.
  862. >>> groups = {"public.kern1.A" : ["A"], "public.kern2.A" : ["A"]}
  863. >>> groupsValidator(groups)
  864. (True, None)
  865. >>> groups = {"public.kern1.A1" : ["A"], "public.kern1.A2" : ["A"]}
  866. >>> valid, msg = groupsValidator(groups)
  867. >>> valid
  868. False
  869. >>> print(msg)
  870. The glyph "A" occurs in too many kerning groups.
  871. """
  872. bogusFormatMessage = "The group data is not in the correct format."
  873. if not isDictEnough(value):
  874. return False, bogusFormatMessage
  875. firstSideMapping: dict[str, str] = {}
  876. secondSideMapping: dict[str, str] = {}
  877. for groupName, glyphList in value.items():
  878. if not isinstance(groupName, (str)):
  879. return False, bogusFormatMessage
  880. if not isinstance(glyphList, (list, tuple)):
  881. return False, bogusFormatMessage
  882. if not groupName:
  883. return False, "A group has an empty name."
  884. if groupName.startswith("public."):
  885. if not groupName.startswith("public.kern1.") and not groupName.startswith(
  886. "public.kern2."
  887. ):
  888. # unknown public.* name. silently skip.
  889. continue
  890. else:
  891. if len("public.kernN.") == len(groupName):
  892. return (
  893. False,
  894. "The group data contains a kerning group with an incomplete name.",
  895. )
  896. if groupName.startswith("public.kern1."):
  897. d = firstSideMapping
  898. else:
  899. d = secondSideMapping
  900. for glyphName in glyphList:
  901. if not isinstance(glyphName, str):
  902. return (
  903. False,
  904. "The group data %s contains an invalid member." % groupName,
  905. )
  906. if glyphName in d:
  907. return (
  908. False,
  909. 'The glyph "%s" occurs in too many kerning groups.' % glyphName,
  910. )
  911. d[glyphName] = groupName
  912. return True, None
  913. # -------------
  914. # kerning.plist
  915. # -------------
  916. def kerningValidator(data: Any) -> tuple[bool, Optional[str]]:
  917. """
  918. Check the validity of the kerning data structure.
  919. Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
  920. >>> kerning = {"A" : {"B" : 100}}
  921. >>> kerningValidator(kerning)
  922. (True, None)
  923. >>> kerning = {"A" : ["B"]}
  924. >>> valid, msg = kerningValidator(kerning)
  925. >>> valid
  926. False
  927. >>> print(msg)
  928. The kerning data is not in the correct format.
  929. >>> kerning = {"A" : {"B" : "100"}}
  930. >>> valid, msg = kerningValidator(kerning)
  931. >>> valid
  932. False
  933. >>> print(msg)
  934. The kerning data is not in the correct format.
  935. """
  936. bogusFormatMessage = "The kerning data is not in the correct format."
  937. if not isinstance(data, Mapping):
  938. return False, bogusFormatMessage
  939. for first, secondDict in data.items():
  940. if not isinstance(first, str):
  941. return False, bogusFormatMessage
  942. elif not isinstance(secondDict, Mapping):
  943. return False, bogusFormatMessage
  944. for second, value in secondDict.items():
  945. if not isinstance(second, str):
  946. return False, bogusFormatMessage
  947. elif not isinstance(value, numberTypes):
  948. return False, bogusFormatMessage
  949. return True, None
  950. # -------------
  951. # lib.plist/lib
  952. # -------------
  953. _bogusLibFormatMessage = "The lib data is not in the correct format: %s"
  954. def fontLibValidator(value: Any) -> tuple[bool, Optional[str]]:
  955. """
  956. Check the validity of the lib.
  957. Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
  958. >>> lib = {"foo" : "bar"}
  959. >>> fontLibValidator(lib)
  960. (True, None)
  961. >>> lib = {"public.awesome" : "hello"}
  962. >>> fontLibValidator(lib)
  963. (True, None)
  964. >>> lib = {"public.glyphOrder" : ["A", "C", "B"]}
  965. >>> fontLibValidator(lib)
  966. (True, None)
  967. >>> lib = "hello"
  968. >>> valid, msg = fontLibValidator(lib)
  969. >>> valid
  970. False
  971. >>> print(msg) # doctest: +ELLIPSIS
  972. The lib data is not in the correct format: expected a dictionary, ...
  973. >>> lib = {1: "hello"}
  974. >>> valid, msg = fontLibValidator(lib)
  975. >>> valid
  976. False
  977. >>> print(msg)
  978. The lib key is not properly formatted: expected str, found int: 1
  979. >>> lib = {"public.glyphOrder" : "hello"}
  980. >>> valid, msg = fontLibValidator(lib)
  981. >>> valid
  982. False
  983. >>> print(msg) # doctest: +ELLIPSIS
  984. public.glyphOrder is not properly formatted: expected list or tuple,...
  985. >>> lib = {"public.glyphOrder" : ["A", 1, "B"]}
  986. >>> valid, msg = fontLibValidator(lib)
  987. >>> valid
  988. False
  989. >>> print(msg) # doctest: +ELLIPSIS
  990. public.glyphOrder is not properly formatted: expected str,...
  991. """
  992. if not isDictEnough(value):
  993. reason = "expected a dictionary, found %s" % type(value).__name__
  994. return False, _bogusLibFormatMessage % reason
  995. for key, value in value.items():
  996. if not isinstance(key, str):
  997. return False, (
  998. "The lib key is not properly formatted: expected str, found %s: %r"
  999. % (type(key).__name__, key)
  1000. )
  1001. # public.glyphOrder
  1002. if key == "public.glyphOrder":
  1003. bogusGlyphOrderMessage = "public.glyphOrder is not properly formatted: %s"
  1004. if not isinstance(value, (list, tuple)):
  1005. reason = "expected list or tuple, found %s" % type(value).__name__
  1006. return False, bogusGlyphOrderMessage % reason
  1007. for glyphName in value:
  1008. if not isinstance(glyphName, str):
  1009. reason = "expected str, found %s" % type(glyphName).__name__
  1010. return False, bogusGlyphOrderMessage % reason
  1011. return True, None
  1012. # --------
  1013. # GLIF lib
  1014. # --------
  1015. def glyphLibValidator(value: Any) -> tuple[bool, Optional[str]]:
  1016. """
  1017. Check the validity of the lib.
  1018. Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
  1019. >>> lib = {"foo" : "bar"}
  1020. >>> glyphLibValidator(lib)
  1021. (True, None)
  1022. >>> lib = {"public.awesome" : "hello"}
  1023. >>> glyphLibValidator(lib)
  1024. (True, None)
  1025. >>> lib = {"public.markColor" : "1,0,0,0.5"}
  1026. >>> glyphLibValidator(lib)
  1027. (True, None)
  1028. >>> lib = {"public.markColor" : 1}
  1029. >>> valid, msg = glyphLibValidator(lib)
  1030. >>> valid
  1031. False
  1032. >>> print(msg)
  1033. public.markColor is not properly formatted.
  1034. """
  1035. if not isDictEnough(value):
  1036. reason = "expected a dictionary, found %s" % type(value).__name__
  1037. return False, _bogusLibFormatMessage % reason
  1038. for key, value in value.items():
  1039. if not isinstance(key, str):
  1040. reason = "key (%s) should be a string" % key
  1041. return False, _bogusLibFormatMessage % reason
  1042. # public.markColor
  1043. if key == "public.markColor":
  1044. if not colorValidator(value):
  1045. return False, "public.markColor is not properly formatted."
  1046. return True, None
  1047. if __name__ == "__main__":
  1048. import doctest
  1049. doctest.testmod()