summaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools/fontTools/ufoLib/validators.py
diff options
context:
space:
mode:
authorshumkovnd <[email protected]>2023-11-10 14:39:34 +0300
committershumkovnd <[email protected]>2023-11-10 16:42:24 +0300
commit77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch)
treec51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/fonttools/fontTools/ufoLib/validators.py
parentdd6d20cadb65582270ac23f4b3b14ae189704b9d (diff)
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/fonttools/fontTools/ufoLib/validators.py')
-rw-r--r--contrib/python/fonttools/fontTools/ufoLib/validators.py1186
1 files changed, 1186 insertions, 0 deletions
diff --git a/contrib/python/fonttools/fontTools/ufoLib/validators.py b/contrib/python/fonttools/fontTools/ufoLib/validators.py
new file mode 100644
index 00000000000..01e3124fd38
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/ufoLib/validators.py
@@ -0,0 +1,1186 @@
+"""Various low level data validators."""
+
+import calendar
+from io import open
+import fs.base
+import fs.osfs
+
+from collections.abc import Mapping
+from fontTools.ufoLib.utils import numberTypes
+
+
+# -------
+# Generic
+# -------
+
+
+def isDictEnough(value):
+ """
+ Some objects will likely come in that aren't
+ dicts but are dict-ish enough.
+ """
+ if isinstance(value, Mapping):
+ return True
+ for attr in ("keys", "values", "items"):
+ if not hasattr(value, attr):
+ return False
+ return True
+
+
+def genericTypeValidator(value, typ):
+ """
+ Generic. (Added at version 2.)
+ """
+ return isinstance(value, typ)
+
+
+def genericIntListValidator(values, validValues):
+ """
+ Generic. (Added at version 2.)
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ valuesSet = set(values)
+ validValuesSet = set(validValues)
+ if valuesSet - validValuesSet:
+ return False
+ for value in values:
+ if not isinstance(value, int):
+ return False
+ return True
+
+
+def genericNonNegativeIntValidator(value):
+ """
+ Generic. (Added at version 3.)
+ """
+ if not isinstance(value, int):
+ return False
+ if value < 0:
+ return False
+ return True
+
+
+def genericNonNegativeNumberValidator(value):
+ """
+ Generic. (Added at version 3.)
+ """
+ if not isinstance(value, numberTypes):
+ return False
+ if value < 0:
+ return False
+ return True
+
+
+def genericDictValidator(value, prototype):
+ """
+ Generic. (Added at version 3.)
+ """
+ # not a dict
+ if not isinstance(value, Mapping):
+ return False
+ # missing required keys
+ for key, (typ, required) in prototype.items():
+ if not required:
+ continue
+ if key not in value:
+ return False
+ # unknown keys
+ for key in value.keys():
+ if key not in prototype:
+ return False
+ # incorrect types
+ for key, v in value.items():
+ prototypeType, required = prototype[key]
+ if v is None and not required:
+ continue
+ if not isinstance(v, prototypeType):
+ return False
+ return True
+
+
+# --------------
+# fontinfo.plist
+# --------------
+
+# Data Validators
+
+
+def fontInfoStyleMapStyleNameValidator(value):
+ """
+ Version 2+.
+ """
+ options = ["regular", "italic", "bold", "bold italic"]
+ return value in options
+
+
+def fontInfoOpenTypeGaspRangeRecordsValidator(value):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ if len(value) == 0:
+ return True
+ validBehaviors = [0, 1, 2, 3]
+ dictPrototype = dict(rangeMaxPPEM=(int, True), rangeGaspBehavior=(list, True))
+ ppemOrder = []
+ for rangeRecord in value:
+ if not genericDictValidator(rangeRecord, dictPrototype):
+ return False
+ ppem = rangeRecord["rangeMaxPPEM"]
+ behavior = rangeRecord["rangeGaspBehavior"]
+ ppemValidity = genericNonNegativeIntValidator(ppem)
+ if not ppemValidity:
+ return False
+ behaviorValidity = genericIntListValidator(behavior, validBehaviors)
+ if not behaviorValidity:
+ return False
+ ppemOrder.append(ppem)
+ if ppemOrder != sorted(ppemOrder):
+ return False
+ return True
+
+
+def fontInfoOpenTypeHeadCreatedValidator(value):
+ """
+ Version 2+.
+ """
+ # format: 0000/00/00 00:00:00
+ if not isinstance(value, str):
+ return False
+ # basic formatting
+ if not len(value) == 19:
+ return False
+ if value.count(" ") != 1:
+ return False
+ date, time = value.split(" ")
+ if date.count("/") != 2:
+ return False
+ if time.count(":") != 2:
+ return False
+ # date
+ year, month, day = date.split("/")
+ if len(year) != 4:
+ return False
+ if len(month) != 2:
+ return False
+ if len(day) != 2:
+ return False
+ try:
+ year = int(year)
+ month = int(month)
+ day = int(day)
+ except ValueError:
+ return False
+ if month < 1 or month > 12:
+ return False
+ monthMaxDay = calendar.monthrange(year, month)[1]
+ if day < 1 or day > monthMaxDay:
+ return False
+ # time
+ hour, minute, second = time.split(":")
+ if len(hour) != 2:
+ return False
+ if len(minute) != 2:
+ return False
+ if len(second) != 2:
+ return False
+ try:
+ hour = int(hour)
+ minute = int(minute)
+ second = int(second)
+ except ValueError:
+ return False
+ if hour < 0 or hour > 23:
+ return False
+ if minute < 0 or minute > 59:
+ return False
+ if second < 0 or second > 59:
+ return False
+ # fallback
+ return True
+
+
+def fontInfoOpenTypeNameRecordsValidator(value):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ dictPrototype = dict(
+ nameID=(int, True),
+ platformID=(int, True),
+ encodingID=(int, True),
+ languageID=(int, True),
+ string=(str, True),
+ )
+ for nameRecord in value:
+ if not genericDictValidator(nameRecord, dictPrototype):
+ return False
+ return True
+
+
+def fontInfoOpenTypeOS2WeightClassValidator(value):
+ """
+ Version 2+.
+ """
+ if not isinstance(value, int):
+ return False
+ if value < 0:
+ return False
+ return True
+
+
+def fontInfoOpenTypeOS2WidthClassValidator(value):
+ """
+ Version 2+.
+ """
+ if not isinstance(value, int):
+ return False
+ if value < 1:
+ return False
+ if value > 9:
+ return False
+ return True
+
+
+def fontInfoVersion2OpenTypeOS2PanoseValidator(values):
+ """
+ Version 2.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) != 10:
+ return False
+ for value in values:
+ if not isinstance(value, int):
+ return False
+ # XXX further validation?
+ return True
+
+
+def fontInfoVersion3OpenTypeOS2PanoseValidator(values):
+ """
+ Version 3+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) != 10:
+ return False
+ for value in values:
+ if not isinstance(value, int):
+ return False
+ if value < 0:
+ return False
+ # XXX further validation?
+ return True
+
+
+def fontInfoOpenTypeOS2FamilyClassValidator(values):
+ """
+ Version 2+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) != 2:
+ return False
+ for value in values:
+ if not isinstance(value, int):
+ return False
+ classID, subclassID = values
+ if classID < 0 or classID > 14:
+ return False
+ if subclassID < 0 or subclassID > 15:
+ return False
+ return True
+
+
+def fontInfoPostscriptBluesValidator(values):
+ """
+ Version 2+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) > 14:
+ return False
+ if len(values) % 2:
+ return False
+ for value in values:
+ if not isinstance(value, numberTypes):
+ return False
+ return True
+
+
+def fontInfoPostscriptOtherBluesValidator(values):
+ """
+ Version 2+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) > 10:
+ return False
+ if len(values) % 2:
+ return False
+ for value in values:
+ if not isinstance(value, numberTypes):
+ return False
+ return True
+
+
+def fontInfoPostscriptStemsValidator(values):
+ """
+ Version 2+.
+ """
+ if not isinstance(values, (list, tuple)):
+ return False
+ if len(values) > 12:
+ return False
+ for value in values:
+ if not isinstance(value, numberTypes):
+ return False
+ return True
+
+
+def fontInfoPostscriptWindowsCharacterSetValidator(value):
+ """
+ Version 2+.
+ """
+ validValues = list(range(1, 21))
+ if value not in validValues:
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataUniqueIDValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(id=(str, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataVendorValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {
+ "name": (str, True),
+ "url": (str, False),
+ "dir": (str, False),
+ "class": (str, False),
+ }
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataCreditsValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(credits=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if not len(value["credits"]):
+ return False
+ dictPrototype = {
+ "name": (str, True),
+ "url": (str, False),
+ "role": (str, False),
+ "dir": (str, False),
+ "class": (str, False),
+ }
+ for credit in value["credits"]:
+ if not genericDictValidator(credit, dictPrototype):
+ return False
+ if "dir" in credit and credit.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataDescriptionValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(url=(str, False), text=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ for text in value["text"]:
+ if not fontInfoWOFFMetadataTextValue(text):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataLicenseValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(url=(str, False), text=(list, False), id=(str, False))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "text" in value:
+ for text in value["text"]:
+ if not fontInfoWOFFMetadataTextValue(text):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataTrademarkValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(text=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ for text in value["text"]:
+ if not fontInfoWOFFMetadataTextValue(text):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataCopyrightValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(text=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ for text in value["text"]:
+ if not fontInfoWOFFMetadataTextValue(text):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataLicenseeValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {"name": (str, True), "dir": (str, False), "class": (str, False)}
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataTextValue(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {
+ "text": (str, True),
+ "language": (str, False),
+ "dir": (str, False),
+ "class": (str, False),
+ }
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataExtensionsValidator(value):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ if not value:
+ return False
+ for extension in value:
+ if not fontInfoWOFFMetadataExtensionValidator(extension):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataExtensionValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(names=(list, False), items=(list, True), id=(str, False))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "names" in value:
+ for name in value["names"]:
+ if not fontInfoWOFFMetadataExtensionNameValidator(name):
+ return False
+ for item in value["items"]:
+ if not fontInfoWOFFMetadataExtensionItemValidator(item):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataExtensionItemValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = dict(id=(str, False), names=(list, True), values=(list, True))
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ for name in value["names"]:
+ if not fontInfoWOFFMetadataExtensionNameValidator(name):
+ return False
+ for val in value["values"]:
+ if not fontInfoWOFFMetadataExtensionValueValidator(val):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataExtensionNameValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {
+ "text": (str, True),
+ "language": (str, False),
+ "dir": (str, False),
+ "class": (str, False),
+ }
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+
+def fontInfoWOFFMetadataExtensionValueValidator(value):
+ """
+ Version 3+.
+ """
+ dictPrototype = {
+ "text": (str, True),
+ "language": (str, False),
+ "dir": (str, False),
+ "class": (str, False),
+ }
+ if not genericDictValidator(value, dictPrototype):
+ return False
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
+ return False
+ return True
+
+
+# ----------
+# Guidelines
+# ----------
+
+
+def guidelinesValidator(value, identifiers=None):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ if identifiers is None:
+ identifiers = set()
+ for guide in value:
+ if not guidelineValidator(guide):
+ return False
+ identifier = guide.get("identifier")
+ if identifier is not None:
+ if identifier in identifiers:
+ return False
+ identifiers.add(identifier)
+ return True
+
+
+_guidelineDictPrototype = dict(
+ x=((int, float), False),
+ y=((int, float), False),
+ angle=((int, float), False),
+ name=(str, False),
+ color=(str, False),
+ identifier=(str, False),
+)
+
+
+def guidelineValidator(value):
+ """
+ Version 3+.
+ """
+ if not genericDictValidator(value, _guidelineDictPrototype):
+ return False
+ x = value.get("x")
+ y = value.get("y")
+ angle = value.get("angle")
+ # x or y must be present
+ if x is None and y is None:
+ return False
+ # if x or y are None, angle must not be present
+ if x is None or y is None:
+ if angle is not None:
+ return False
+ # if x and y are defined, angle must be defined
+ if x is not None and y is not None and angle is None:
+ return False
+ # angle must be between 0 and 360
+ if angle is not None:
+ if angle < 0:
+ return False
+ if angle > 360:
+ return False
+ # identifier must be 1 or more characters
+ identifier = value.get("identifier")
+ if identifier is not None and not identifierValidator(identifier):
+ return False
+ # color must follow the proper format
+ color = value.get("color")
+ if color is not None and not colorValidator(color):
+ return False
+ return True
+
+
+# -------
+# Anchors
+# -------
+
+
+def anchorsValidator(value, identifiers=None):
+ """
+ Version 3+.
+ """
+ if not isinstance(value, list):
+ return False
+ if identifiers is None:
+ identifiers = set()
+ for anchor in value:
+ if not anchorValidator(anchor):
+ return False
+ identifier = anchor.get("identifier")
+ if identifier is not None:
+ if identifier in identifiers:
+ return False
+ identifiers.add(identifier)
+ return True
+
+
+_anchorDictPrototype = dict(
+ x=((int, float), False),
+ y=((int, float), False),
+ name=(str, False),
+ color=(str, False),
+ identifier=(str, False),
+)
+
+
+def anchorValidator(value):
+ """
+ Version 3+.
+ """
+ if not genericDictValidator(value, _anchorDictPrototype):
+ return False
+ x = value.get("x")
+ y = value.get("y")
+ # x and y must be present
+ if x is None or y is None:
+ return False
+ # identifier must be 1 or more characters
+ identifier = value.get("identifier")
+ if identifier is not None and not identifierValidator(identifier):
+ return False
+ # color must follow the proper format
+ color = value.get("color")
+ if color is not None and not colorValidator(color):
+ return False
+ return True
+
+
+# ----------
+# Identifier
+# ----------
+
+
+def identifierValidator(value):
+ """
+ Version 3+.
+
+ >>> identifierValidator("a")
+ True
+ >>> identifierValidator("")
+ False
+ >>> identifierValidator("a" * 101)
+ False
+ """
+ validCharactersMin = 0x20
+ validCharactersMax = 0x7E
+ if not isinstance(value, str):
+ return False
+ if not value:
+ return False
+ if len(value) > 100:
+ return False
+ for c in value:
+ c = ord(c)
+ if c < validCharactersMin or c > validCharactersMax:
+ return False
+ return True
+
+
+# -----
+# Color
+# -----
+
+
+def colorValidator(value):
+ """
+ Version 3+.
+
+ >>> colorValidator("0,0,0,0")
+ True
+ >>> colorValidator(".5,.5,.5,.5")
+ True
+ >>> colorValidator("0.5,0.5,0.5,0.5")
+ True
+ >>> colorValidator("1,1,1,1")
+ True
+
+ >>> colorValidator("2,0,0,0")
+ False
+ >>> colorValidator("0,2,0,0")
+ False
+ >>> colorValidator("0,0,2,0")
+ False
+ >>> colorValidator("0,0,0,2")
+ False
+
+ >>> colorValidator("1r,1,1,1")
+ False
+ >>> colorValidator("1,1g,1,1")
+ False
+ >>> colorValidator("1,1,1b,1")
+ False
+ >>> colorValidator("1,1,1,1a")
+ False
+
+ >>> colorValidator("1 1 1 1")
+ False
+ >>> colorValidator("1 1,1,1")
+ False
+ >>> colorValidator("1,1 1,1")
+ False
+ >>> colorValidator("1,1,1 1")
+ False
+
+ >>> colorValidator("1, 1, 1, 1")
+ True
+ """
+ if not isinstance(value, str):
+ return False
+ parts = value.split(",")
+ if len(parts) != 4:
+ return False
+ for part in parts:
+ part = part.strip()
+ converted = False
+ try:
+ part = int(part)
+ converted = True
+ except ValueError:
+ pass
+ if not converted:
+ try:
+ part = float(part)
+ converted = True
+ except ValueError:
+ pass
+ if not converted:
+ return False
+ if part < 0:
+ return False
+ if part > 1:
+ return False
+ return True
+
+
+# -----
+# image
+# -----
+
+pngSignature = b"\x89PNG\r\n\x1a\n"
+
+_imageDictPrototype = dict(
+ fileName=(str, True),
+ xScale=((int, float), False),
+ xyScale=((int, float), False),
+ yxScale=((int, float), False),
+ yScale=((int, float), False),
+ xOffset=((int, float), False),
+ yOffset=((int, float), False),
+ color=(str, False),
+)
+
+
+def imageValidator(value):
+ """
+ Version 3+.
+ """
+ if not genericDictValidator(value, _imageDictPrototype):
+ return False
+ # fileName must be one or more characters
+ if not value["fileName"]:
+ return False
+ # color must follow the proper format
+ color = value.get("color")
+ if color is not None and not colorValidator(color):
+ return False
+ return True
+
+
+def pngValidator(path=None, data=None, fileObj=None):
+ """
+ Version 3+.
+
+ This checks the signature of the image data.
+ """
+ assert path is not None or data is not None or fileObj is not None
+ if path is not None:
+ with open(path, "rb") as f:
+ signature = f.read(8)
+ elif data is not None:
+ signature = data[:8]
+ elif fileObj is not None:
+ pos = fileObj.tell()
+ signature = fileObj.read(8)
+ fileObj.seek(pos)
+ if signature != pngSignature:
+ return False, "Image does not begin with the PNG signature."
+ return True, None
+
+
+# -------------------
+# layercontents.plist
+# -------------------
+
+
+def layerContentsValidator(value, ufoPathOrFileSystem):
+ """
+ Check the validity of layercontents.plist.
+ Version 3+.
+ """
+ if isinstance(ufoPathOrFileSystem, fs.base.FS):
+ fileSystem = ufoPathOrFileSystem
+ else:
+ fileSystem = fs.osfs.OSFS(ufoPathOrFileSystem)
+
+ bogusFileMessage = "layercontents.plist in not in the correct format."
+ # file isn't in the right format
+ if not isinstance(value, list):
+ return False, bogusFileMessage
+ # work through each entry
+ usedLayerNames = set()
+ usedDirectories = set()
+ contents = {}
+ for entry in value:
+ # layer entry in the incorrect format
+ if not isinstance(entry, list):
+ return False, bogusFileMessage
+ if not len(entry) == 2:
+ return False, bogusFileMessage
+ for i in entry:
+ if not isinstance(i, str):
+ return False, bogusFileMessage
+ layerName, directoryName = entry
+ # check directory naming
+ if directoryName != "glyphs":
+ if not directoryName.startswith("glyphs."):
+ return (
+ False,
+ "Invalid directory name (%s) in layercontents.plist."
+ % directoryName,
+ )
+ if len(layerName) == 0:
+ return False, "Empty layer name in layercontents.plist."
+ # directory doesn't exist
+ if not fileSystem.exists(directoryName):
+ return False, "A glyphset does not exist at %s." % directoryName
+ # default layer name
+ if layerName == "public.default" and directoryName != "glyphs":
+ return (
+ False,
+ "The name public.default is being used by a layer that is not the default.",
+ )
+ # check usage
+ if layerName in usedLayerNames:
+ return (
+ False,
+ "The layer name %s is used by more than one layer." % layerName,
+ )
+ usedLayerNames.add(layerName)
+ if directoryName in usedDirectories:
+ return (
+ False,
+ "The directory %s is used by more than one layer." % directoryName,
+ )
+ usedDirectories.add(directoryName)
+ # store
+ contents[layerName] = directoryName
+ # missing default layer
+ foundDefault = "glyphs" in contents.values()
+ if not foundDefault:
+ return False, "The required default glyph set is not in the UFO."
+ return True, None
+
+
+# ------------
+# groups.plist
+# ------------
+
+
+def groupsValidator(value):
+ """
+ Check the validity of the groups.
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
+
+ >>> groups = {"A" : ["A", "A"], "A2" : ["A"]}
+ >>> groupsValidator(groups)
+ (True, None)
+
+ >>> groups = {"" : ["A"]}
+ >>> valid, msg = groupsValidator(groups)
+ >>> valid
+ False
+ >>> print(msg)
+ A group has an empty name.
+
+ >>> groups = {"public.awesome" : ["A"]}
+ >>> groupsValidator(groups)
+ (True, None)
+
+ >>> groups = {"public.kern1." : ["A"]}
+ >>> valid, msg = groupsValidator(groups)
+ >>> valid
+ False
+ >>> print(msg)
+ The group data contains a kerning group with an incomplete name.
+ >>> groups = {"public.kern2." : ["A"]}
+ >>> valid, msg = groupsValidator(groups)
+ >>> valid
+ False
+ >>> print(msg)
+ The group data contains a kerning group with an incomplete name.
+
+ >>> groups = {"public.kern1.A" : ["A"], "public.kern2.A" : ["A"]}
+ >>> groupsValidator(groups)
+ (True, None)
+
+ >>> groups = {"public.kern1.A1" : ["A"], "public.kern1.A2" : ["A"]}
+ >>> valid, msg = groupsValidator(groups)
+ >>> valid
+ False
+ >>> print(msg)
+ The glyph "A" occurs in too many kerning groups.
+ """
+ bogusFormatMessage = "The group data is not in the correct format."
+ if not isDictEnough(value):
+ return False, bogusFormatMessage
+ firstSideMapping = {}
+ secondSideMapping = {}
+ for groupName, glyphList in value.items():
+ if not isinstance(groupName, (str)):
+ return False, bogusFormatMessage
+ if not isinstance(glyphList, (list, tuple)):
+ return False, bogusFormatMessage
+ if not groupName:
+ return False, "A group has an empty name."
+ if groupName.startswith("public."):
+ if not groupName.startswith("public.kern1.") and not groupName.startswith(
+ "public.kern2."
+ ):
+ # unknown public.* name. silently skip.
+ continue
+ else:
+ if len("public.kernN.") == len(groupName):
+ return (
+ False,
+ "The group data contains a kerning group with an incomplete name.",
+ )
+ if groupName.startswith("public.kern1."):
+ d = firstSideMapping
+ else:
+ d = secondSideMapping
+ for glyphName in glyphList:
+ if not isinstance(glyphName, str):
+ return (
+ False,
+ "The group data %s contains an invalid member." % groupName,
+ )
+ if glyphName in d:
+ return (
+ False,
+ 'The glyph "%s" occurs in too many kerning groups.' % glyphName,
+ )
+ d[glyphName] = groupName
+ return True, None
+
+
+# -------------
+# kerning.plist
+# -------------
+
+
+def kerningValidator(data):
+ """
+ Check the validity of the kerning data structure.
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
+
+ >>> kerning = {"A" : {"B" : 100}}
+ >>> kerningValidator(kerning)
+ (True, None)
+
+ >>> kerning = {"A" : ["B"]}
+ >>> valid, msg = kerningValidator(kerning)
+ >>> valid
+ False
+ >>> print(msg)
+ The kerning data is not in the correct format.
+
+ >>> kerning = {"A" : {"B" : "100"}}
+ >>> valid, msg = kerningValidator(kerning)
+ >>> valid
+ False
+ >>> print(msg)
+ The kerning data is not in the correct format.
+ """
+ bogusFormatMessage = "The kerning data is not in the correct format."
+ if not isinstance(data, Mapping):
+ return False, bogusFormatMessage
+ for first, secondDict in data.items():
+ if not isinstance(first, str):
+ return False, bogusFormatMessage
+ elif not isinstance(secondDict, Mapping):
+ return False, bogusFormatMessage
+ for second, value in secondDict.items():
+ if not isinstance(second, str):
+ return False, bogusFormatMessage
+ elif not isinstance(value, numberTypes):
+ return False, bogusFormatMessage
+ return True, None
+
+
+# -------------
+# lib.plist/lib
+# -------------
+
+_bogusLibFormatMessage = "The lib data is not in the correct format: %s"
+
+
+def fontLibValidator(value):
+ """
+ Check the validity of the lib.
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
+
+ >>> lib = {"foo" : "bar"}
+ >>> fontLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.awesome" : "hello"}
+ >>> fontLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.glyphOrder" : ["A", "C", "B"]}
+ >>> fontLibValidator(lib)
+ (True, None)
+
+ >>> lib = "hello"
+ >>> valid, msg = fontLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg) # doctest: +ELLIPSIS
+ The lib data is not in the correct format: expected a dictionary, ...
+
+ >>> lib = {1: "hello"}
+ >>> valid, msg = fontLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg)
+ The lib key is not properly formatted: expected str, found int: 1
+
+ >>> lib = {"public.glyphOrder" : "hello"}
+ >>> valid, msg = fontLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg) # doctest: +ELLIPSIS
+ public.glyphOrder is not properly formatted: expected list or tuple,...
+
+ >>> lib = {"public.glyphOrder" : ["A", 1, "B"]}
+ >>> valid, msg = fontLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg) # doctest: +ELLIPSIS
+ public.glyphOrder is not properly formatted: expected str,...
+ """
+ if not isDictEnough(value):
+ reason = "expected a dictionary, found %s" % type(value).__name__
+ return False, _bogusLibFormatMessage % reason
+ for key, value in value.items():
+ if not isinstance(key, str):
+ return False, (
+ "The lib key is not properly formatted: expected str, found %s: %r"
+ % (type(key).__name__, key)
+ )
+ # public.glyphOrder
+ if key == "public.glyphOrder":
+ bogusGlyphOrderMessage = "public.glyphOrder is not properly formatted: %s"
+ if not isinstance(value, (list, tuple)):
+ reason = "expected list or tuple, found %s" % type(value).__name__
+ return False, bogusGlyphOrderMessage % reason
+ for glyphName in value:
+ if not isinstance(glyphName, str):
+ reason = "expected str, found %s" % type(glyphName).__name__
+ return False, bogusGlyphOrderMessage % reason
+ return True, None
+
+
+# --------
+# GLIF lib
+# --------
+
+
+def glyphLibValidator(value):
+ """
+ Check the validity of the lib.
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
+
+ >>> lib = {"foo" : "bar"}
+ >>> glyphLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.awesome" : "hello"}
+ >>> glyphLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.markColor" : "1,0,0,0.5"}
+ >>> glyphLibValidator(lib)
+ (True, None)
+
+ >>> lib = {"public.markColor" : 1}
+ >>> valid, msg = glyphLibValidator(lib)
+ >>> valid
+ False
+ >>> print(msg)
+ public.markColor is not properly formatted.
+ """
+ if not isDictEnough(value):
+ reason = "expected a dictionary, found %s" % type(value).__name__
+ return False, _bogusLibFormatMessage % reason
+ for key, value in value.items():
+ if not isinstance(key, str):
+ reason = "key (%s) should be a string" % key
+ return False, _bogusLibFormatMessage % reason
+ # public.markColor
+ if key == "public.markColor":
+ if not colorValidator(value):
+ return False, "public.markColor is not properly formatted."
+ return True, None
+
+
+if __name__ == "__main__":
+ import doctest
+
+ doctest.testmod()