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