1187 lines
30 KiB
Python
1187 lines
30 KiB
Python
"""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()
|