1from __future__ import absolute_import, unicode_literals
2import os
3import shutil
4from io import StringIO, BytesIO, open
5from copy import deepcopy
6from fontTools.misc.py23 import basestring, unicode, tounicode
7from ufoLib.glifLib import GlyphSet
8from ufoLib.validators import *
9from ufoLib.filenames import userNameToFileName
10from ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
11from ufoLib import plistlib
12"""
13A library for importing .ufo files and their descendants.
14Refer to http://unifiedfontobject.com for the UFO specification.
15
16The UFOReader and UFOWriter classes support versions 1, 2 and 3
17of the specification.
18
19Sets that list the font info attribute names for the fontinfo.plist
20formats are available for external use. These are:
21	fontInfoAttributesVersion1
22	fontInfoAttributesVersion2
23	fontInfoAttributesVersion3
24
25A set listing the fontinfo.plist attributes that were deprecated
26in version 2 is available for external use:
27	deprecatedFontInfoAttributesVersion2
28
29Functions that do basic validation on values for fontinfo.plist
30are available for external use. These are
31	validateFontInfoVersion2ValueForAttribute
32	validateFontInfoVersion3ValueForAttribute
33
34Value conversion functions are available for converting
35fontinfo.plist values between the possible format versions.
36	convertFontInfoValueForAttributeFromVersion1ToVersion2
37	convertFontInfoValueForAttributeFromVersion2ToVersion1
38	convertFontInfoValueForAttributeFromVersion2ToVersion3
39	convertFontInfoValueForAttributeFromVersion3ToVersion2
40"""
41
42__all__ = [
43	"makeUFOPath",
44	"UFOLibError",
45	"UFOReader",
46	"UFOWriter",
47	"fontInfoAttributesVersion1",
48	"fontInfoAttributesVersion2",
49	"fontInfoAttributesVersion3",
50	"deprecatedFontInfoAttributesVersion2",
51	"validateFontInfoVersion2ValueForAttribute",
52	"validateFontInfoVersion3ValueForAttribute",
53	"convertFontInfoValueForAttributeFromVersion1ToVersion2",
54	"convertFontInfoValueForAttributeFromVersion2ToVersion1",
55	# deprecated
56	"convertUFOFormatVersion1ToFormatVersion2",
57]
58
59__version__ = "2.3.2"
60
61
62class UFOLibError(Exception): pass
63
64
65# ----------
66# File Names
67# ----------
68
69DEFAULT_GLYPHS_DIRNAME = "glyphs"
70DATA_DIRNAME = "data"
71IMAGES_DIRNAME = "images"
72METAINFO_FILENAME = "metainfo.plist"
73FONTINFO_FILENAME = "fontinfo.plist"
74LIB_FILENAME = "lib.plist"
75GROUPS_FILENAME = "groups.plist"
76KERNING_FILENAME = "kerning.plist"
77FEATURES_FILENAME = "features.fea"
78LAYERCONTENTS_FILENAME = "layercontents.plist"
79LAYERINFO_FILENAME = "layerinfo.plist"
80
81DEFAULT_LAYER_NAME = "public.default"
82
83supportedUFOFormatVersions = [1, 2, 3]
84
85
86# --------------
87# Shared Methods
88# --------------
89
90def _getPlist(self, fileName, default=None):
91	"""
92	Read a property list relative to the
93	path argument of UFOReader. If the file
94	is missing and default is None a
95	UFOLibError will be raised otherwise
96	default is returned. The errors that
97	could be raised during the reading of
98	a plist are unpredictable and/or too
99	large to list, so, a blind try: except:
100	is done. If an exception occurs, a
101	UFOLibError will be raised.
102	"""
103	path = os.path.join(self._path, fileName)
104	if not os.path.exists(path):
105		if default is not None:
106			return default
107		else:
108			raise UFOLibError("%s is missing in %s. This file is required" % (fileName, self._path))
109	try:
110		with open(path, "rb") as f:
111			return plistlib.load(f)
112	except:
113		raise UFOLibError("The file %s could not be read." % fileName)
114
115# ----------
116# UFO Reader
117# ----------
118
119class UFOReader(object):
120
121	"""
122	Read the various components of the .ufo.
123
124	By default read data is validated. Set ``validate`` to
125	``False`` to not validate the data.
126	"""
127
128	def __init__(self, path, validate=True):
129		if not os.path.exists(path):
130			raise UFOLibError("The specified UFO doesn't exist.")
131		self._path = path
132		self._validate = validate
133		self.readMetaInfo(validate=validate)
134		self._upConvertedKerningData = None
135
136	# properties
137
138	def _get_formatVersion(self):
139		return self._formatVersion
140
141	formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.")
142
143	# up conversion
144
145	def _upConvertKerning(self, validate):
146		"""
147		Up convert kerning and groups in UFO 1 and 2.
148		The data will be held internally until each bit of data
149		has been retrieved. The conversion of both must be done
150		at once, so the raw data is cached and an error is raised
151		if one bit of data becomes obsolete before it is called.
152
153		``validate`` will validate the data.
154		"""
155		if self._upConvertedKerningData:
156			testKerning = self._readKerning()
157			if testKerning != self._upConvertedKerningData["originalKerning"]:
158				raise UFOLibError("The data in kerning.plist has been modified since it was converted to UFO 3 format.")
159			testGroups = self._readGroups()
160			if testGroups != self._upConvertedKerningData["originalGroups"]:
161				raise UFOLibError("The data in groups.plist has been modified since it was converted to UFO 3 format.")
162		else:
163			groups = self._readGroups()
164			if validate:
165				invalidFormatMessage = "groups.plist is not properly formatted."
166				if not isinstance(groups, dict):
167					raise UFOLibError(invalidFormatMessage)
168				for groupName, glyphList in list(groups.items()):
169					if not isinstance(groupName, basestring):
170						raise UFOLibError(invalidFormatMessage)
171					elif not isinstance(glyphList, list):
172						raise UFOLibError(invalidFormatMessage)
173					for glyphName in glyphList:
174						if not isinstance(glyphName, basestring):
175							raise UFOLibError(invalidFormatMessage)
176			self._upConvertedKerningData = dict(
177				kerning={},
178				originalKerning=self._readKerning(),
179				groups={},
180				originalGroups=groups
181			)
182			# convert kerning and groups
183			kerning, groups, conversionMaps = convertUFO1OrUFO2KerningToUFO3Kerning(
184				self._upConvertedKerningData["originalKerning"],
185				deepcopy(self._upConvertedKerningData["originalGroups"])
186			)
187			# store
188			self._upConvertedKerningData["kerning"] = kerning
189			self._upConvertedKerningData["groups"] = groups
190			self._upConvertedKerningData["groupRenameMaps"] = conversionMaps
191
192	# support methods
193
194	_checkForFile = staticmethod(os.path.exists)
195
196	_getPlist = _getPlist
197
198	def readBytesFromPath(self, path, encoding=None):
199		"""
200		Returns the bytes in the file at the given path.
201		The path must be relative to the UFO path.
202		Returns None if the file does not exist.
203		An encoding may be passed if needed.
204		"""
205		fullPath = os.path.join(self._path, path)
206		if not self._checkForFile(fullPath):
207			return None
208		if os.path.isdir(fullPath):
209			raise UFOLibError("%s is a directory." % path)
210		if encoding:
211			f = open(fullPath, encoding=encoding)
212		else:
213			f = open(fullPath, "rb", encoding=encoding)
214		data = f.read()
215		f.close()
216		return data
217
218	def getReadFileForPath(self, path, encoding=None):
219		"""
220		Returns a file (or file-like) object for the
221		file at the given path. The path must be relative
222		to the UFO path. Returns None if the file does not exist.
223		An encoding may be passed if needed.
224
225		Note: The caller is responsible for closing the open file.
226		"""
227		fullPath = os.path.join(self._path, path)
228		if not self._checkForFile(fullPath):
229			return None
230		if os.path.isdir(fullPath):
231			raise UFOLibError("%s is a directory." % path)
232		if encoding:
233			f = open(fullPath, "rb", encoding=encoding)
234		else:
235			f = open(fullPath, "r")
236		return f
237
238	def getFileModificationTime(self, path):
239		"""
240		Returns the modification time (as reported by os.path.getmtime)
241		for the file at the given path. The path must be relative to
242		the UFO path. Returns None if the file does not exist.
243		"""
244		fullPath = os.path.join(self._path, path)
245		if not self._checkForFile(fullPath):
246			return None
247		return os.path.getmtime(fullPath)
248
249	# metainfo.plist
250
251	def readMetaInfo(self, validate=None):
252		"""
253		Read metainfo.plist. Only used for internal operations.
254
255		``validate`` will validate the read data, by default it is set
256		to the class's validate value, can be overridden.
257		"""
258		if validate is None:
259			validate = self._validate
260		# should there be a blind try/except with a UFOLibError
261		# raised in except here (and elsewhere)? It would be nice to
262		# provide external callers with a single exception to catch.
263		data = self._getPlist(METAINFO_FILENAME)
264		if validate and not isinstance(data, dict):
265			raise UFOLibError("metainfo.plist is not properly formatted.")
266		formatVersion = data["formatVersion"]
267		if validate:
268			if not isinstance(formatVersion, int):
269				metaplist_path = os.path.join(self._path, METAINFO_FILENAME)
270				raise UFOLibError("formatVersion must be specified as an integer in " + metaplist_path)
271			if formatVersion not in supportedUFOFormatVersions:
272				raise UFOLibError("Unsupported UFO format (%d) in %s." % (formatVersion, self._path))
273		self._formatVersion = formatVersion
274
275	# groups.plist
276
277	def _readGroups(self):
278		return self._getPlist(GROUPS_FILENAME, {})
279
280	def readGroups(self, validate=None):
281		"""
282		Read groups.plist. Returns a dict.
283		``validate`` will validate the read data, by default it is set to the
284		class's validate value, can be overridden.
285		"""
286		if validate is None:
287			validate = self._validate
288		# handle up conversion
289		if self._formatVersion < 3:
290			self._upConvertKerning(validate)
291			groups = self._upConvertedKerningData["groups"]
292		# normal
293		else:
294			groups = self._readGroups()
295		if validate:
296			valid, message = groupsValidator(groups)
297			if not valid:
298				raise UFOLibError(message)
299		return groups
300
301	def getKerningGroupConversionRenameMaps(self, validate=None):
302		"""
303		Get maps defining the renaming that was done during any
304		needed kerning group conversion. This method returns a
305		dictionary of this form:
306
307			{
308				"side1" : {"old group name" : "new group name"},
309				"side2" : {"old group name" : "new group name"}
310			}
311
312		When no conversion has been performed, the side1 and side2
313		dictionaries will be empty.
314
315		``validate`` will validate the groups, by default it is set to the
316		class's validate value, can be overridden.
317		"""
318		if validate is None:
319			validate = self._validate
320		if self._formatVersion >= 3:
321			return dict(side1={}, side2={})
322		# use the public group reader to force the load and
323		# conversion of the data if it hasn't happened yet.
324		self.readGroups(validate=validate)
325		return self._upConvertedKerningData["groupRenameMaps"]
326
327	# fontinfo.plist
328
329	def _readInfo(self, validate):
330		data = self._getPlist(FONTINFO_FILENAME, {})
331		if validate and not isinstance(data, dict):
332			raise UFOLibError("fontinfo.plist is not properly formatted.")
333		return data
334
335	def readInfo(self, info, validate=None):
336		"""
337		Read fontinfo.plist. It requires an object that allows
338		setting attributes with names that follow the fontinfo.plist
339		version 3 specification. This will write the attributes
340		defined in the file into the object.
341
342		``validate`` will validate the read data, by default it is set to the
343		class's validate value, can be overridden.
344		"""
345		if validate is None:
346			validate = self._validate
347		infoDict = self._readInfo(validate)
348		infoDataToSet = {}
349		# version 1
350		if self._formatVersion == 1:
351			for attr in fontInfoAttributesVersion1:
352				value = infoDict.get(attr)
353				if value is not None:
354					infoDataToSet[attr] = value
355			infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet)
356			infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
357		# version 2
358		elif self._formatVersion == 2:
359			for attr, dataValidationDict in list(fontInfoAttributesVersion2ValueData.items()):
360				value = infoDict.get(attr)
361				if value is None:
362					continue
363				infoDataToSet[attr] = value
364			infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
365		# version 3
366		elif self._formatVersion == 3:
367			for attr, dataValidationDict in list(fontInfoAttributesVersion3ValueData.items()):
368				value = infoDict.get(attr)
369				if value is None:
370					continue
371				infoDataToSet[attr] = value
372		# unsupported version
373		else:
374			raise NotImplementedError
375		# validate data
376		if validate:
377			infoDataToSet = validateInfoVersion3Data(infoDataToSet)
378		# populate the object
379		for attr, value in list(infoDataToSet.items()):
380			try:
381				setattr(info, attr, value)
382			except AttributeError:
383				raise UFOLibError("The supplied info object does not support setting a necessary attribute (%s)." % attr)
384
385	# kerning.plist
386
387	def _readKerning(self):
388		data = self._getPlist(KERNING_FILENAME, {})
389		return data
390
391	def readKerning(self, validate=None):
392		"""
393		Read kerning.plist. Returns a dict.
394
395		``validate`` will validate the kerning data, by default it is set to the
396		class's validate value, can be overridden.
397		"""
398		if validate is None:
399			validate = self._validate
400		# handle up conversion
401		if self._formatVersion < 3:
402			self._upConvertKerning(validate)
403			kerningNested = self._upConvertedKerningData["kerning"]
404		# normal
405		else:
406			kerningNested = self._readKerning()
407		if validate:
408			valid, message = kerningValidator(kerningNested)
409			if not valid:
410				raise UFOLibError(message)
411		# flatten
412		kerning = {}
413		for left in kerningNested:
414			for right in kerningNested[left]:
415				value = kerningNested[left][right]
416				kerning[left, right] = value
417		return kerning
418
419	# lib.plist
420
421	def readLib(self, validate=None):
422		"""
423		Read lib.plist. Returns a dict.
424
425		``validate`` will validate the data, by default it is set to the
426		class's validate value, can be overridden.
427		"""
428		if validate is None:
429			validate = self._validate
430		data = self._getPlist(LIB_FILENAME, {})
431		if validate:
432			valid, message = fontLibValidator(data)
433			if not valid:
434				raise UFOLibError(message)
435		return data
436
437	# features.fea
438
439	def readFeatures(self):
440		"""
441		Read features.fea. Returns a string.
442		"""
443		path = os.path.join(self._path, FEATURES_FILENAME)
444		if not self._checkForFile(path):
445			return ""
446		with open(path, "r", encoding="utf-8") as f:
447			text = f.read()
448		return text
449
450	# glyph sets & layers
451
452	def _readLayerContents(self, validate):
453		"""
454		Rebuild the layer contents list by checking what glyphsets
455		are available on disk.
456
457		``validate`` will validate the layer contents.
458		"""
459		if self._formatVersion < 3:
460			return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
461		# read the file on disk
462		contents = self._getPlist(LAYERCONTENTS_FILENAME)
463		if validate:
464			valid, error = layerContentsValidator(contents, self._path)
465			if not valid:
466				raise UFOLibError(error)
467		return contents
468
469	def getLayerNames(self, validate=None):
470		"""
471		Get the ordered layer names from layercontents.plist.
472
473		``validate`` will validate the data, by default it is set to the
474		class's validate value, can be overridden.
475		"""
476		if validate is None:
477			validate = self._validate
478		layerContents = self._readLayerContents(validate)
479		layerNames = [layerName for layerName, directoryName in layerContents]
480		return layerNames
481
482	def getDefaultLayerName(self, validate=None):
483		"""
484		Get the default layer name from layercontents.plist.
485
486		``validate`` will validate the data, by default it is set to the
487		class's validate value, can be overridden.
488		"""
489		if validate is None:
490			validate = self._validate
491		layerContents = self._readLayerContents(validate)
492		for layerName, layerDirectory in layerContents:
493			if layerDirectory == DEFAULT_GLYPHS_DIRNAME:
494				return layerName
495		# this will already have been raised during __init__
496		raise UFOLibError("The default layer is not defined in layercontents.plist.")
497
498	def getGlyphSet(self, layerName=None, validateRead=None, validateWrite=None):
499		"""
500		Return the GlyphSet associated with the
501		glyphs directory mapped to layerName
502		in the UFO. If layerName is not provided,
503		the name retrieved with getDefaultLayerName
504		will be used.
505
506		``validateRead`` will validate the read data, by default it is set to the
507		class's validate value, can be overridden.
508		``validateWrte`` will validate the written data, by default it is set to the
509		class's validate value, can be overridden.
510		"""
511		if validateRead is None:
512			validateRead = self._validate
513		if validateWrite is None:
514			validateWrite = self._validate
515		if layerName is None:
516			layerName = self.getDefaultLayerName(validate=validateRead)
517		directory = None
518		layerContents = self._readLayerContents(validateRead)
519		for storedLayerName, storedLayerDirectory in layerContents:
520			if layerName == storedLayerName:
521				directory = storedLayerDirectory
522				break
523		if directory is None:
524			raise UFOLibError("No glyphs directory is mapped to \"%s\"." % layerName)
525		glyphsPath = os.path.join(self._path, directory)
526		return GlyphSet(glyphsPath, ufoFormatVersion=self._formatVersion, validateRead=validateRead, validateWrite=validateWrite)
527
528	def getCharacterMapping(self, layerName=None, validate=None):
529		"""
530		Return a dictionary that maps unicode values (ints) to
531		lists of glyph names.
532		"""
533		if validate is None:
534			validate = self._validate
535		glyphSet = self.getGlyphSet(layerName, validateRead=validate, validateWrite=True)
536		allUnicodes = glyphSet.getUnicodes()
537		cmap = {}
538		for glyphName, unicodes in allUnicodes.items():
539			for code in unicodes:
540				if code in cmap:
541					cmap[code].append(glyphName)
542				else:
543					cmap[code] = [glyphName]
544		return cmap
545
546	# /data
547
548	def getDataDirectoryListing(self, maxDepth=100):
549		"""
550		Returns a list of all files in the data directory.
551		The returned paths will be relative to the UFO.
552		This will not list directory names, only file names.
553		Thus, empty directories will be skipped.
554
555		The maxDepth argument sets the maximum number
556		of sub-directories that are allowed.
557		"""
558		path = os.path.join(self._path, DATA_DIRNAME)
559		if not self._checkForFile(path):
560			return []
561		listing = self._getDirectoryListing(path, maxDepth=maxDepth)
562		listing = [os.path.relpath(path, "data") for path in listing]
563		return listing
564
565	def _getDirectoryListing(self, path, depth=0, maxDepth=100):
566		if depth > maxDepth:
567			raise UFOLibError("Maximum recusion depth reached.")
568		result = []
569		for fileName in os.listdir(path):
570			p = os.path.join(path, fileName)
571			if os.path.isdir(p):
572				result += self._getDirectoryListing(p, depth=depth+1, maxDepth=maxDepth)
573			else:
574				p = os.path.relpath(p, self._path)
575				result.append(p)
576		return result
577
578	def getImageDirectoryListing(self, validate=None):
579		"""
580		Returns a list of all image file names in
581		the images directory. Each of the images will
582		have been verified to have the PNG signature.
583
584		``validate`` will validate the data, by default it is set to the
585		class's validate value, can be overridden.
586		"""
587		if validate is None:
588			validate = self._validate
589		if self._formatVersion < 3:
590			return []
591		path = os.path.join(self._path, IMAGES_DIRNAME)
592		if not os.path.exists(path):
593			return []
594		if not os.path.isdir(path):
595			raise UFOLibError("The UFO contains an \"images\" file instead of a directory.")
596		result = []
597		for fileName in os.listdir(path):
598			p = os.path.join(path, fileName)
599			if os.path.isdir(p):
600				# silently skip this as version control
601				# systems often have hidden directories
602				continue
603			if validate:
604				valid, error = pngValidator(path=p)
605				if valid:
606					result.append(fileName)
607			else:
608				result.append(fileName)
609		return result
610
611	def readImage(self, fileName, validate=None):
612		"""
613		Return image data for the file named fileName.
614
615		``validate`` will validate the data, by default it is set to the
616		class's validate value, can be overridden.
617		"""
618		if validate is None:
619			validate = self._validate
620		if self._formatVersion < 3:
621			raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion)
622		data = self.readBytesFromPath(os.path.join(IMAGES_DIRNAME, fileName))
623		if data is None:
624			raise UFOLibError("No image file named %s." % fileName)
625		if validate:
626			valid, error = pngValidator(data=data)
627			if not valid:
628				raise UFOLibError(error)
629		return data
630
631# ----------
632# UFO Writer
633# ----------
634
635
636class UFOWriter(object):
637
638	"""
639	Write the various components of the .ufo.
640
641	By default, the written data will be validated before writing. Set ``validate`` to
642	``False`` if you do not want to validate the data. Validation can also be overriden
643	on a per method level if desired.
644	"""
645
646	def __init__(self, path, formatVersion=3, fileCreator="org.robofab.ufoLib", validate=True):
647		if formatVersion not in supportedUFOFormatVersions:
648			raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
649		# establish some basic stuff
650		self._path = path
651		self._formatVersion = formatVersion
652		self._fileCreator = fileCreator
653		self._downConversionKerningData = None
654		self._validate = validate
655
656		# if the file already exists, get the format version.
657		# this will be needed for up and down conversion.
658		previousFormatVersion = None
659		if os.path.exists(path):
660			metaInfo = self._getPlist(METAINFO_FILENAME)
661			previousFormatVersion = metaInfo.get("formatVersion")
662			try:
663				previousFormatVersion = int(previousFormatVersion)
664			except:
665				raise UFOLibError("The existing metainfo.plist is not properly formatted.")
666			if previousFormatVersion not in supportedUFOFormatVersions:
667				raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
668		# catch down conversion
669		if previousFormatVersion is not None and previousFormatVersion > formatVersion:
670			raise UFOLibError("The UFO located at this path is a higher version (%d) than the version (%d) that is trying to be written. This is not supported." % (previousFormatVersion, formatVersion))
671		# handle the layer contents
672		self.layerContents = {}
673		if previousFormatVersion is not None and previousFormatVersion >= 3:
674			# already exists
675			self._readLayerContents(validate=validate)
676		else:
677			# previous < 3
678			# imply the layer contents
679			p = os.path.join(path, DEFAULT_GLYPHS_DIRNAME)
680			if os.path.exists(p):
681				self.layerContents = {DEFAULT_LAYER_NAME : DEFAULT_GLYPHS_DIRNAME}
682		# write the new metainfo
683		self._writeMetaInfo()
684
685	# properties
686
687	def _get_path(self):
688		return self._path
689
690	path = property(_get_path, doc="The path the UFO is being written to.")
691
692	def _get_formatVersion(self):
693		return self._formatVersion
694
695	formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is set into metainfo.plist during __init__.")
696
697	def _get_fileCreator(self):
698		return self._fileCreator
699
700	fileCreator = property(_get_fileCreator, doc="The file creator of the UFO. This is set into metainfo.plist during __init__.")
701
702	# support methods
703
704	_getPlist = _getPlist
705
706	def _writePlist(self, fileName, data):
707		"""
708		Write a property list. The errors that
709		could be raised during the writing of
710		a plist are unpredictable and/or too
711		large to list, so, a blind try: except:
712		is done. If an exception occurs, a
713		UFOLibError will be raised.
714		"""
715		self._makeDirectory()
716		path = os.path.join(self._path, fileName)
717		try:
718			data = writePlistAtomically(data, path)
719		except:
720			raise UFOLibError("The data for the file %s could not be written because it is not properly formatted." % fileName)
721
722	def _deleteFile(self, fileName):
723		path = os.path.join(self._path, fileName)
724		if os.path.exists(path):
725			os.remove(path)
726
727	def _makeDirectory(self, subDirectory=None):
728		path = self._path
729		if subDirectory:
730			path = os.path.join(self._path, subDirectory)
731		if not os.path.exists(path):
732			os.makedirs(path)
733		return path
734
735	def _buildDirectoryTree(self, path):
736		directory, fileName = os.path.split(path)
737		directoryTree = []
738		while directory:
739			directory, d = os.path.split(directory)
740			directoryTree.append(d)
741		directoryTree.reverse()
742		built = ""
743		for d in directoryTree:
744			d = os.path.join(built, d)
745			p = os.path.join(self._path, d)
746			if not os.path.exists(p):
747				os.mkdir(p)
748			built = d
749
750	def _removeFileForPath(self, path, raiseErrorIfMissing=False):
751		originalPath = path
752		path = os.path.join(self._path, path)
753		if not os.path.exists(path):
754			if raiseErrorIfMissing:
755				raise UFOLibError("The file %s does not exist." % path)
756		else:
757			if os.path.isdir(path):
758				shutil.rmtree(path)
759			else:
760				os.remove(path)
761		# remove any directories that are now empty
762		self._removeEmptyDirectoriesForPath(os.path.dirname(originalPath))
763
764	def _removeEmptyDirectoriesForPath(self, directory):
765		absoluteDirectory = os.path.join(self._path, directory)
766		if not os.path.exists(absoluteDirectory):
767			return
768		if not len(os.listdir(absoluteDirectory)):
769			shutil.rmtree(absoluteDirectory)
770		else:
771			return
772		directory = os.path.dirname(directory)
773		if directory:
774			self._removeEmptyDirectoriesForPath(directory)
775
776	# file system interaction
777
778	def writeBytesToPath(self, path, data, encoding=None):
779		"""
780		Write bytes to path. If needed, the directory tree
781		for the given path will be built. The path must be
782		relative to the UFO. An encoding may be passed if needed.
783		"""
784		fullPath = os.path.join(self._path, path)
785		if os.path.exists(fullPath) and os.path.isdir(fullPath):
786			raise UFOLibError("A directory exists at %s." % path)
787		self._buildDirectoryTree(path)
788		if encoding:
789			data = StringIO(data).encode(encoding)
790		writeDataFileAtomically(data, fullPath)
791
792	def getFileObjectForPath(self, path, encoding=None):
793		"""
794		Creates a write mode file object at path. If needed,
795		the directory tree for the given path will be built.
796		The path must be relative to the UFO. An encoding may
797		be passed if needed.
798
799		Note: The caller is responsible for closing the open file.
800		"""
801		fullPath = os.path.join(self._path, path)
802		if os.path.exists(fullPath) and os.path.isdir(fullPath):
803			raise UFOLibError("A directory exists at %s." % path)
804		self._buildDirectoryTree(path)
805		return open(fullPath, "w", encoding=encoding)
806
807	def removeFileForPath(self, path):
808		"""
809		Remove the file (or directory) at path. The path
810		must be relative to the UFO. This is only allowed
811		for files in the data and image directories.
812		"""
813		# make sure that only data or images is being changed
814		d = path
815		parts = []
816		while d:
817			d, p = os.path.split(d)
818			if p:
819				parts.append(p)
820		if parts[-1] not in ("images", "data"):
821			raise UFOLibError("Removing \"%s\" is not legal." % path)
822		# remove the file
823		self._removeFileForPath(path, raiseErrorIfMissing=True)
824
825	def copyFromReader(self, reader, sourcePath, destPath):
826		"""
827		Copy the sourcePath in the provided UFOReader to destPath
828		in this writer. The paths must be relative. They may represent
829		directories or paths. This uses the most memory efficient
830		method possible for copying the data possible.
831		"""
832		if not isinstance(reader, UFOReader):
833			raise UFOLibError("The reader must be an instance of UFOReader.")
834		fullSourcePath = os.path.join(reader._path, sourcePath)
835		if not reader._checkForFile(fullSourcePath):
836			raise UFOLibError("No file named \"%s\" to copy from." % sourcePath)
837		fullDestPath = os.path.join(self._path, destPath)
838		if os.path.exists(fullDestPath):
839			raise UFOLibError("A file named \"%s\" already exists." % sourcePath)
840		self._buildDirectoryTree(destPath)
841		if os.path.isdir(fullSourcePath):
842			shutil.copytree(fullSourcePath, fullDestPath)
843		else:
844			shutil.copy(fullSourcePath, fullDestPath)
845
846	# UFO mod time
847
848	def setModificationTime(self):
849		"""
850		Set the UFO modification time to the current time.
851		This is never called automatically. It is up to the
852		caller to call this when finished working on the UFO.
853		"""
854		os.utime(self._path, None)
855
856	# metainfo.plist
857
858	def _writeMetaInfo(self):
859		metaInfo = dict(
860			creator=self._fileCreator,
861			formatVersion=self._formatVersion
862		)
863		self._writePlist(METAINFO_FILENAME, metaInfo)
864
865	# groups.plist
866
867	def setKerningGroupConversionRenameMaps(self, maps):
868		"""
869		Set maps defining the renaming that should be done
870		when writing groups and kerning in UFO 1 and UFO 2.
871		This will effectively undo the conversion done when
872		UFOReader reads this data. The dictionary should have
873		this form:
874
875			{
876				"side1" : {"group name to use when writing" : "group name in data"},
877				"side2" : {"group name to use when writing" : "group name in data"}
878			}
879
880		This is the same form returned by UFOReader's
881		getKerningGroupConversionRenameMaps method.
882		"""
883		if self._formatVersion >= 3:
884			return # XXX raise an error here
885		# flip the dictionaries
886		remap = {}
887		for side in ("side1", "side2"):
888			for writeName, dataName in list(maps[side].items()):
889				remap[dataName] = writeName
890		self._downConversionKerningData = dict(groupRenameMap=remap)
891
892	def writeGroups(self, groups, validate=None):
893		"""
894		Write groups.plist. This method requires a
895		dict of glyph groups as an argument.
896
897		``validate`` will validate the data, by default it is set to the
898		class's validate value, can be overridden.
899		"""
900		if validate is None:
901			validate = self._validate
902		# validate the data structure
903		if validate:
904			valid, message = groupsValidator(groups)
905			if not valid:
906				raise UFOLibError(message)
907		# down convert
908		if self._formatVersion < 3 and self._downConversionKerningData is not None:
909			remap = self._downConversionKerningData["groupRenameMap"]
910			remappedGroups = {}
911			# there are some edge cases here that are ignored:
912			# 1. if a group is being renamed to a name that
913			#    already exists, the existing group is always
914			#    overwritten. (this is why there are two loops
915			#    below.) there doesn't seem to be a logical
916			#    solution to groups mismatching and overwriting
917			#    with the specifiecd group seems like a better
918			#    solution than throwing an error.
919			# 2. if side 1 and side 2 groups are being renamed
920			#    to the same group name there is no check to
921			#    ensure that the contents are identical. that
922			#    is left up to the caller.
923			for name, contents in list(groups.items()):
924				if name in remap:
925					continue
926				remappedGroups[name] = contents
927			for name, contents in list(groups.items()):
928				if name not in remap:
929					continue
930				name = remap[name]
931				remappedGroups[name] = contents
932			groups = remappedGroups
933		# pack and write
934		groupsNew = {}
935		for key, value in list(groups.items()):
936			groupsNew[key] = list(value)
937		if groupsNew:
938			self._writePlist(GROUPS_FILENAME, groupsNew)
939		else:
940			self._deleteFile(GROUPS_FILENAME)
941
942	# fontinfo.plist
943
944	def writeInfo(self, info, validate=None):
945		"""
946		Write info.plist. This method requires an object
947		that supports getting attributes that follow the
948		fontinfo.plist version 2 specification. Attributes
949		will be taken from the given object and written
950		into the file.
951
952		``validate`` will validate the data, by default it is set to the
953		class's validate value, can be overridden.
954		"""
955		if validate is None:
956			validate = self._validate
957		# gather version 3 data
958		infoData = {}
959		for attr in list(fontInfoAttributesVersion3ValueData.keys()):
960			if hasattr(info, attr):
961				try:
962					value = getattr(info, attr)
963				except AttributeError:
964					raise UFOLibError("The supplied info object does not support getting a necessary attribute (%s)." % attr)
965				if value is None:
966					continue
967				infoData[attr] = value
968		# down convert data if necessary and validate
969		if self._formatVersion == 3:
970			if validate:
971				infoData = validateInfoVersion3Data(infoData)
972		elif self._formatVersion == 2:
973			infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
974			if validate:
975				infoData = validateInfoVersion2Data(infoData)
976		elif self._formatVersion == 1:
977			infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
978			if validate:
979				infoData = validateInfoVersion2Data(infoData)
980			infoData = _convertFontInfoDataVersion2ToVersion1(infoData)
981		# write file
982		self._writePlist(FONTINFO_FILENAME, infoData)
983
984	# kerning.plist
985
986	def writeKerning(self, kerning, validate=None):
987		"""
988		Write kerning.plist. This method requires a
989		dict of kerning pairs as an argument.
990
991		This performs basic structural validation of the kerning,
992		but it does not check for compliance with the spec in
993		regards to conflicting pairs. The assumption is that the
994		kerning data being passed is standards compliant.
995
996		``validate`` will validate the data, by default it is set to the
997		class's validate value, can be overridden.
998		"""
999		if validate is None:
1000			validate = self._validate
1001		# validate the data structure
1002		if validate:
1003			invalidFormatMessage = "The kerning is not properly formatted."
1004			if not isDictEnough(kerning):
1005				raise UFOLibError(invalidFormatMessage)
1006			for pair, value in list(kerning.items()):
1007				if not isinstance(pair, (list, tuple)):
1008					raise UFOLibError(invalidFormatMessage)
1009				if not len(pair) == 2:
1010					raise UFOLibError(invalidFormatMessage)
1011				if not isinstance(pair[0], basestring):
1012					raise UFOLibError(invalidFormatMessage)
1013				if not isinstance(pair[1], basestring):
1014					raise UFOLibError(invalidFormatMessage)
1015				if not isinstance(value, (int, float)):
1016					raise UFOLibError(invalidFormatMessage)
1017		# down convert
1018		if self._formatVersion < 3 and self._downConversionKerningData is not None:
1019			remap = self._downConversionKerningData["groupRenameMap"]
1020			remappedKerning = {}
1021			for (side1, side2), value in list(kerning.items()):
1022				side1 = remap.get(side1, side1)
1023				side2 = remap.get(side2, side2)
1024				remappedKerning[side1, side2] = value
1025			kerning = remappedKerning
1026		# pack and write
1027		kerningDict = {}
1028		for left, right in list(kerning.keys()):
1029			value = kerning[left, right]
1030			if left not in kerningDict:
1031				kerningDict[left] = {}
1032			kerningDict[left][right] = value
1033		if kerningDict:
1034			self._writePlist(KERNING_FILENAME, kerningDict)
1035		else:
1036			self._deleteFile(KERNING_FILENAME)
1037
1038	# lib.plist
1039
1040	def writeLib(self, libDict, validate=None):
1041		"""
1042		Write lib.plist. This method requires a
1043		lib dict as an argument.
1044
1045		``validate`` will validate the data, by default it is set to the
1046		class's validate value, can be overridden.
1047		"""
1048		if validate is None:
1049			validate = self._validate
1050		if validate:
1051			valid, message = fontLibValidator(libDict)
1052			if not valid:
1053				raise UFOLibError(message)
1054		if libDict:
1055			self._writePlist(LIB_FILENAME, libDict)
1056		else:
1057			self._deleteFile(LIB_FILENAME)
1058
1059	# features.fea
1060
1061	def writeFeatures(self, features, validate=None):
1062		"""
1063		Write features.fea. This method requires a
1064		features string as an argument.
1065		"""
1066		if validate is None:
1067			validate = self._validate
1068		if self._formatVersion == 1:
1069			raise UFOLibError("features.fea is not allowed in UFO Format Version 1.")
1070		if validate:
1071			if not isinstance(features, basestring):
1072				raise UFOLibError("The features are not text.")
1073		self._makeDirectory()
1074		path = os.path.join(self._path, FEATURES_FILENAME)
1075		writeFileAtomically(features, path)
1076
1077	# glyph sets & layers
1078
1079	def _readLayerContents(self, validate):
1080		"""
1081		Rebuild the layer contents list by checking what glyph sets
1082		are available on disk.
1083
1084		``validate`` will validate the data.
1085		"""
1086		# read the file on disk
1087		raw = self._getPlist(LAYERCONTENTS_FILENAME)
1088		contents = {}
1089		if validate:
1090			valid, error = layerContentsValidator(raw, self._path)
1091			if not valid:
1092				raise UFOLibError(error)
1093		for entry in raw:
1094			layerName, directoryName = entry
1095			contents[layerName] = directoryName
1096		self.layerContents = contents
1097
1098	def writeLayerContents(self, layerOrder=None, validate=None):
1099		"""
1100		Write the layercontents.plist file. This method  *must* be called
1101		after all glyph sets have been written.
1102		"""
1103		if validate is None:
1104			validate = self._validate
1105		if self.formatVersion < 3:
1106			return
1107		if layerOrder is not None:
1108			newOrder = []
1109			for layerName in layerOrder:
1110				if layerName is None:
1111					layerName = DEFAULT_LAYER_NAME
1112				else:
1113					layerName = tounicode(layerName)
1114				newOrder.append(layerName)
1115			layerOrder = newOrder
1116		else:
1117			layerOrder = list(self.layerContents.keys())
1118		if validate and set(layerOrder) != set(self.layerContents.keys()):
1119			raise UFOLibError("The layer order content does not match the glyph sets that have been created.")
1120		layerContents = [(layerName, self.layerContents[layerName]) for layerName in layerOrder]
1121		self._writePlist(LAYERCONTENTS_FILENAME, layerContents)
1122
1123	def _findDirectoryForLayerName(self, layerName):
1124		foundDirectory = None
1125		for existingLayerName, directoryName in list(self.layerContents.items()):
1126			if layerName is None and directoryName == DEFAULT_GLYPHS_DIRNAME:
1127				foundDirectory = directoryName
1128				break
1129			elif existingLayerName == layerName:
1130				foundDirectory = directoryName
1131				break
1132		if not foundDirectory:
1133			raise UFOLibError("Could not locate a glyph set directory for the layer named %s." % layerName)
1134		return foundDirectory
1135
1136	def getGlyphSet(self, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None, validateRead=None, validateWrite=None):
1137		"""
1138		Return the GlyphSet object associated with the
1139		appropriate glyph directory in the .ufo.
1140		If layerName is None, the default glyph set
1141		will be used. The defaultLayer flag indictes
1142		that the layer should be saved into the default
1143		glyphs directory.
1144
1145		``validateRead`` will validate the read data, by default it is set to the
1146		class's validate value, can be overridden.
1147		``validateWrte`` will validate the written data, by default it is set to the
1148		class's validate value, can be overridden.
1149		"""
1150		if validateRead is None:
1151			validateRead = self._validate
1152		if validateWrite is None:
1153			validateWrite = self._validate
1154		# only default can be written in < 3
1155		if self._formatVersion < 3 and (not defaultLayer or layerName is not None):
1156			raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion)
1157		# locate a layer name when None has been given
1158		if layerName is None and defaultLayer:
1159			for existingLayerName, directory in list(self.layerContents.items()):
1160				if directory == DEFAULT_GLYPHS_DIRNAME:
1161					layerName = existingLayerName
1162			if layerName is None:
1163				layerName = DEFAULT_LAYER_NAME
1164		elif layerName is None and not defaultLayer:
1165			raise UFOLibError("A layer name must be provided for non-default layers.")
1166		# move along to format specific writing
1167		if self.formatVersion == 1:
1168			return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
1169		elif self.formatVersion == 2:
1170			return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
1171		elif self.formatVersion == 3:
1172			return self._getGlyphSetFormatVersion3(validateRead, validateWrite, layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
1173
1174	def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
1175		glyphDir = self._makeDirectory(DEFAULT_GLYPHS_DIRNAME)
1176		return GlyphSet(glyphDir, glyphNameToFileNameFunc, ufoFormatVersion=1, validateRead=validateRead, validateWrite=validateWrite)
1177
1178	def _getGlyphSetFormatVersion2(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
1179		glyphDir = self._makeDirectory(DEFAULT_GLYPHS_DIRNAME)
1180		return GlyphSet(glyphDir, glyphNameToFileNameFunc, ufoFormatVersion=2, validateRead=validateRead, validateWrite=validateWrite)
1181
1182	def _getGlyphSetFormatVersion3(self, validateRead, validateWrite, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None):
1183		# if the default flag is on, make sure that the default in the file
1184		# matches the default being written. also make sure that this layer
1185		# name is not already linked to a non-default layer.
1186		if defaultLayer:
1187			for existingLayerName, directory in list(self.layerContents.items()):
1188				if directory == DEFAULT_GLYPHS_DIRNAME:
1189					if existingLayerName != layerName:
1190						raise UFOLibError("Another layer is already mapped to the default directory.")
1191				elif existingLayerName == layerName:
1192					raise UFOLibError("The layer name is already mapped to a non-default layer.")
1193		# get an existing directory name
1194		if layerName in self.layerContents:
1195			directory = self.layerContents[layerName]
1196		# get a  new directory name
1197		else:
1198			if defaultLayer:
1199				directory = DEFAULT_GLYPHS_DIRNAME
1200			else:
1201				# not caching this could be slightly expensive,
1202				# but caching it will be cumbersome
1203				existing = [d.lower() for d in list(self.layerContents.values())]
1204				if not isinstance(layerName, unicode):
1205					try:
1206						layerName = unicode(layerName)
1207					except UnicodeDecodeError:
1208						raise UFOLibError("The specified layer name is not a Unicode string.")
1209				directory = userNameToFileName(layerName, existing=existing, prefix="glyphs.")
1210		# make the directory
1211		path = os.path.join(self._path, directory)
1212		if not os.path.exists(path):
1213			self._makeDirectory(subDirectory=directory)
1214		# store the mapping
1215		self.layerContents[layerName] = directory
1216		# load the glyph set
1217		return GlyphSet(path, glyphNameToFileNameFunc=glyphNameToFileNameFunc, ufoFormatVersion=3, validateRead=validateRead, validateWrite=validateWrite)
1218
1219	def renameGlyphSet(self, layerName, newLayerName, defaultLayer=False):
1220		"""
1221		Rename a glyph set.
1222
1223		Note: if a GlyphSet object has already been retrieved for
1224		layerName, it is up to the caller to inform that object that
1225		the directory it represents has changed.
1226		"""
1227		if self._formatVersion < 3:
1228			# ignore renaming glyph sets for UFO1 UFO2
1229			# just write the data from the default layer
1230			return
1231		# the new and old names can be the same
1232		# as long as the default is being switched
1233		if layerName == newLayerName:
1234			# if the default is off and the layer is already not the default, skip
1235			if self.layerContents[layerName] != DEFAULT_GLYPHS_DIRNAME and not defaultLayer:
1236				return
1237			# if the default is on and the layer is already the default, skip
1238			if self.layerContents[layerName] == DEFAULT_GLYPHS_DIRNAME and defaultLayer:
1239				return
1240		else:
1241			# make sure the new layer name doesn't already exist
1242			if newLayerName is None:
1243				newLayerName = DEFAULT_LAYER_NAME
1244			if newLayerName in self.layerContents:
1245				raise UFOLibError("A layer named %s already exists." % newLayerName)
1246			# make sure the default layer doesn't already exist
1247			if defaultLayer and DEFAULT_GLYPHS_DIRNAME in list(self.layerContents.values()):
1248				raise UFOLibError("A default layer already exists.")
1249		# get the paths
1250		oldDirectory = self._findDirectoryForLayerName(layerName)
1251		if defaultLayer:
1252			newDirectory = DEFAULT_GLYPHS_DIRNAME
1253		else:
1254			existing = [name.lower() for name in list(self.layerContents.values())]
1255			newDirectory = userNameToFileName(newLayerName, existing=existing, prefix="glyphs.")
1256		# update the internal mapping
1257		del self.layerContents[layerName]
1258		self.layerContents[newLayerName] = newDirectory
1259		# do the file system copy
1260		oldDirectory = os.path.join(self._path, oldDirectory)
1261		newDirectory = os.path.join(self._path, newDirectory)
1262		shutil.move(oldDirectory, newDirectory)
1263
1264	def deleteGlyphSet(self, layerName):
1265		"""
1266		Remove the glyph set matching layerName.
1267		"""
1268		if self._formatVersion < 3:
1269			# ignore deleting glyph sets for UFO1 UFO2 as there are no layers
1270			# just write the data from the default layer
1271			return
1272		foundDirectory = self._findDirectoryForLayerName(layerName)
1273		self._removeFileForPath(foundDirectory)
1274		del self.layerContents[layerName]
1275
1276	# /images
1277
1278	def writeImage(self, fileName, data, validate=None):
1279		"""
1280		Write data to fileName in the images directory.
1281		The data must be a valid PNG.
1282		"""
1283		if validate is None:
1284			validate = self._validate
1285		if self._formatVersion < 3:
1286			raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
1287		if validate:
1288			valid, error = pngValidator(data=data)
1289			if not valid:
1290				raise UFOLibError(error)
1291		path = os.path.join(IMAGES_DIRNAME, fileName)
1292		self.writeBytesToPath(path, data)
1293
1294	def removeImage(self, fileName, validate=None):
1295		"""
1296		Remove the file named fileName from the
1297		images directory.
1298		"""
1299		if validate is None:
1300			validate = self._validate
1301		if self._formatVersion < 3:
1302			raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
1303		path = os.path.join(IMAGES_DIRNAME, fileName)
1304		self.removeFileForPath(path)
1305
1306	def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None):
1307		"""
1308		Copy the sourceFileName in the provided UFOReader to destFileName
1309		in this writer. This uses the most memory efficient method possible
1310		for copying the data possible.
1311		"""
1312		if validate is None:
1313			validate = self._validate
1314		if self._formatVersion < 3:
1315			raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
1316		sourcePath = os.path.join("images", sourceFileName)
1317		destPath = os.path.join("images", destFileName)
1318		self.copyFromReader(reader, sourcePath, destPath)
1319
1320
1321# ----------------
1322# Helper Functions
1323# ----------------
1324
1325def makeUFOPath(path):
1326	"""
1327	Return a .ufo pathname.
1328
1329	>>> makeUFOPath("directory/something.ext") == (
1330	... 	os.path.join('directory', 'something.ufo'))
1331	True
1332	>>> makeUFOPath("directory/something.another.thing.ext") == (
1333	... 	os.path.join('directory', 'something.another.thing.ufo'))
1334	True
1335	"""
1336	dir, name = os.path.split(path)
1337	name = ".".join([".".join(name.split(".")[:-1]), "ufo"])
1338	return os.path.join(dir, name)
1339
1340def writePlistAtomically(obj, path):
1341	"""
1342	Write a plist for "obj" to "path". Do this sort of atomically,
1343	making it harder to cause corrupt files, for example when writePlist
1344	encounters an error halfway during write. This also checks to see
1345	if text matches the text that is already in the file at path.
1346	If so, the file is not rewritten so that the modification date
1347	is preserved.
1348	"""
1349	data = plistlib.dumps(obj)
1350	writeDataFileAtomically(data, path)
1351
1352def writeFileAtomically(text, path, encoding="utf-8"):
1353	"""
1354	Write text into a file at path. Do this sort of atomically
1355	making it harder to cause corrupt files. This also checks to see
1356	if text matches the text that is already in the file at path.
1357	If so, the file is not rewritten so that the modification date
1358	is preserved. An encoding may be passed if needed.
1359	"""
1360	if os.path.exists(path):
1361		with open(path, "r", encoding=encoding) as f:
1362			oldText = f.read()
1363		if text == oldText:
1364			return
1365		# if the text is empty, remove the existing file
1366		if not text:
1367			os.remove(path)
1368	if text:
1369		with open(path, "w", encoding=encoding) as f:
1370			f.write(text)
1371
1372def writeDataFileAtomically(data, path):
1373	"""
1374	Write data into a file at path. Do this sort of atomically
1375	making it harder to cause corrupt files. This also checks to see
1376	if data matches the data that is already in the file at path.
1377	If so, the file is not rewritten so that the modification date
1378	is preserved.
1379	"""
1380	assert isinstance(data, bytes)
1381	if os.path.exists(path):
1382		f = open(path, "rb")
1383		oldData = f.read()
1384		f.close()
1385		if data == oldData:
1386			return
1387		# if the data is empty, remove the existing file
1388		if not data:
1389			os.remove(path)
1390	if data:
1391		f = open(path, "wb")
1392		f.write(data)
1393		f.close()
1394
1395# ---------------------------
1396# Format Conversion Functions
1397# ---------------------------
1398
1399def convertUFOFormatVersion1ToFormatVersion2(inPath, outPath=None, validateRead=False, validateWrite=True):
1400	"""
1401	Function for converting a version format 1 UFO
1402	to version format 2. inPath should be a path
1403	to a UFO. outPath is the path where the new UFO
1404	should be written. If outPath is not given, the
1405	inPath will be used and, therefore, the UFO will
1406	be converted in place. Otherwise, if outPath is
1407	specified, nothing must exist at that path.
1408
1409	``validateRead`` will validate the read data.
1410	``validateWrite`` will validate the written data.
1411	"""
1412	from warnings import warn
1413	warn("convertUFOFormatVersion1ToFormatVersion2 is deprecated.", DeprecationWarning)
1414	if outPath is None:
1415		outPath = inPath
1416	if inPath != outPath and os.path.exists(outPath):
1417		raise UFOLibError("A file already exists at %s." % outPath)
1418	# use a reader for loading most of the data
1419	reader = UFOReader(inPath, validate=validateRead)
1420	if reader.formatVersion == 2:
1421		raise UFOLibError("The UFO at %s is already format version 2." % inPath)
1422	groups = reader.readGroups()
1423	kerning = reader.readKerning()
1424	libData = reader.readLib()
1425	# read the info data manually and convert
1426	infoPath = os.path.join(inPath, FONTINFO_FILENAME)
1427	if not os.path.exists(infoPath):
1428		infoData = {}
1429	else:
1430		with open(infoPath, "rb") as f:
1431			infoData = plistlib.load(f)
1432	infoData = _convertFontInfoDataVersion1ToVersion2(infoData)
1433	# if the paths are the same, only need to change the
1434	# fontinfo and meta info files.
1435	infoPath = os.path.join(outPath, FONTINFO_FILENAME)
1436	if inPath == outPath:
1437		metaInfoPath = os.path.join(inPath, METAINFO_FILENAME)
1438		metaInfo = dict(
1439			creator="org.robofab.ufoLib",
1440			formatVersion=2
1441		)
1442		writePlistAtomically(metaInfo, metaInfoPath)
1443		writePlistAtomically(infoData, infoPath)
1444	# otherwise write everything.
1445	else:
1446		writer = UFOWriter(outPath, formatVersion=2, validate=validateWrite)
1447		writer.writeGroups(groups)
1448		writer.writeKerning(kerning)
1449		writer.writeLib(libData)
1450		# write the info manually
1451		writePlistAtomically(infoData, infoPath)
1452		# copy the glyph tree
1453		inGlyphs = os.path.join(inPath, DEFAULT_GLYPHS_DIRNAME)
1454		outGlyphs = os.path.join(outPath, DEFAULT_GLYPHS_DIRNAME)
1455		if os.path.exists(inGlyphs):
1456			shutil.copytree(inGlyphs, outGlyphs)
1457
1458# ----------------------
1459# fontinfo.plist Support
1460# ----------------------
1461
1462# Version Validators
1463
1464# There is no version 1 validator and there shouldn't be.
1465# The version 1 spec was very loose and there were numerous
1466# cases of invalid values.
1467
1468def validateFontInfoVersion2ValueForAttribute(attr, value):
1469	"""
1470	This performs very basic validation of the value for attribute
1471	following the UFO 2 fontinfo.plist specification. The results
1472	of this should not be interpretted as *correct* for the font
1473	that they are part of. This merely indicates that the value
1474	is of the proper type and, where the specification defines
1475	a set range of possible values for an attribute, that the
1476	value is in the accepted range.
1477	"""
1478	dataValidationDict = fontInfoAttributesVersion2ValueData[attr]
1479	valueType = dataValidationDict.get("type")
1480	validator = dataValidationDict.get("valueValidator")
1481	valueOptions = dataValidationDict.get("valueOptions")
1482	# have specific options for the validator
1483	if valueOptions is not None:
1484		isValidValue = validator(value, valueOptions)
1485	# no specific options
1486	else:
1487		if validator == genericTypeValidator:
1488			isValidValue = validator(value, valueType)
1489		else:
1490			isValidValue = validator(value)
1491	return isValidValue
1492
1493def validateInfoVersion2Data(infoData):
1494	"""
1495	This performs very basic validation of the value for infoData
1496	following the UFO 2 fontinfo.plist specification. The results
1497	of this should not be interpretted as *correct* for the font
1498	that they are part of. This merely indicates that the values
1499	are of the proper type and, where the specification defines
1500	a set range of possible values for an attribute, that the
1501	value is in the accepted range.
1502	"""
1503	validInfoData = {}
1504	for attr, value in list(infoData.items()):
1505		isValidValue = validateFontInfoVersion2ValueForAttribute(attr, value)
1506		if not isValidValue:
1507			raise UFOLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
1508		else:
1509			validInfoData[attr] = value
1510	return validInfoData
1511
1512def validateFontInfoVersion3ValueForAttribute(attr, value):
1513	"""
1514	This performs very basic validation of the value for attribute
1515	following the UFO 3 fontinfo.plist specification. The results
1516	of this should not be interpretted as *correct* for the font
1517	that they are part of. This merely indicates that the value
1518	is of the proper type and, where the specification defines
1519	a set range of possible values for an attribute, that the
1520	value is in the accepted range.
1521	"""
1522	dataValidationDict = fontInfoAttributesVersion3ValueData[attr]
1523	valueType = dataValidationDict.get("type")
1524	validator = dataValidationDict.get("valueValidator")
1525	valueOptions = dataValidationDict.get("valueOptions")
1526	# have specific options for the validator
1527	if valueOptions is not None:
1528		isValidValue = validator(value, valueOptions)
1529	# no specific options
1530	else:
1531		if validator == genericTypeValidator:
1532			isValidValue = validator(value, valueType)
1533		else:
1534			isValidValue = validator(value)
1535	return isValidValue
1536
1537def validateInfoVersion3Data(infoData):
1538	"""
1539	This performs very basic validation of the value for infoData
1540	following the UFO 3 fontinfo.plist specification. The results
1541	of this should not be interpretted as *correct* for the font
1542	that they are part of. This merely indicates that the values
1543	are of the proper type and, where the specification defines
1544	a set range of possible values for an attribute, that the
1545	value is in the accepted range.
1546	"""
1547	validInfoData = {}
1548	for attr, value in list(infoData.items()):
1549		isValidValue = validateFontInfoVersion3ValueForAttribute(attr, value)
1550		if not isValidValue:
1551			raise UFOLibError("Invalid value for attribute %s (%s)." % (attr, repr(value)))
1552		else:
1553			validInfoData[attr] = value
1554	return validInfoData
1555
1556# Value Options
1557
1558fontInfoOpenTypeHeadFlagsOptions = list(range(0, 15))
1559fontInfoOpenTypeOS2SelectionOptions = [1, 2, 3, 4, 7, 8, 9]
1560fontInfoOpenTypeOS2UnicodeRangesOptions = list(range(0, 128))
1561fontInfoOpenTypeOS2CodePageRangesOptions = list(range(0, 64))
1562fontInfoOpenTypeOS2TypeOptions = [0, 1, 2, 3, 8, 9]
1563
1564# Version Attribute Definitions
1565# This defines the attributes, types and, in some
1566# cases the possible values, that can exist is
1567# fontinfo.plist.
1568
1569fontInfoAttributesVersion1 = set([
1570	"familyName",
1571	"styleName",
1572	"fullName",
1573	"fontName",
1574	"menuName",
1575	"fontStyle",
1576	"note",
1577	"versionMajor",
1578	"versionMinor",
1579	"year",
1580	"copyright",
1581	"notice",
1582	"trademark",
1583	"license",
1584	"licenseURL",
1585	"createdBy",
1586	"designer",
1587	"designerURL",
1588	"vendorURL",
1589	"unitsPerEm",
1590	"ascender",
1591	"descender",
1592	"capHeight",
1593	"xHeight",
1594	"defaultWidth",
1595	"slantAngle",
1596	"italicAngle",
1597	"widthName",
1598	"weightName",
1599	"weightValue",
1600	"fondName",
1601	"otFamilyName",
1602	"otStyleName",
1603	"otMacName",
1604	"msCharSet",
1605	"fondID",
1606	"uniqueID",
1607	"ttVendor",
1608	"ttUniqueID",
1609	"ttVersion",
1610])
1611
1612fontInfoAttributesVersion2ValueData = {
1613	"familyName"							: dict(type=basestring),
1614	"styleName"								: dict(type=basestring),
1615	"styleMapFamilyName"					: dict(type=basestring),
1616	"styleMapStyleName"						: dict(type=basestring, valueValidator=fontInfoStyleMapStyleNameValidator),
1617	"versionMajor"							: dict(type=int),
1618	"versionMinor"							: dict(type=int),
1619	"year"									: dict(type=int),
1620	"copyright"								: dict(type=basestring),
1621	"trademark"								: dict(type=basestring),
1622	"unitsPerEm"							: dict(type=(int, float)),
1623	"descender"								: dict(type=(int, float)),
1624	"xHeight"								: dict(type=(int, float)),
1625	"capHeight"								: dict(type=(int, float)),
1626	"ascender"								: dict(type=(int, float)),
1627	"italicAngle"							: dict(type=(float, int)),
1628	"note"									: dict(type=basestring),
1629	"openTypeHeadCreated"					: dict(type=basestring, valueValidator=fontInfoOpenTypeHeadCreatedValidator),
1630	"openTypeHeadLowestRecPPEM"				: dict(type=(int, float)),
1631	"openTypeHeadFlags"						: dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeHeadFlagsOptions),
1632	"openTypeHheaAscender"					: dict(type=(int, float)),
1633	"openTypeHheaDescender"					: dict(type=(int, float)),
1634	"openTypeHheaLineGap"					: dict(type=(int, float)),
1635	"openTypeHheaCaretSlopeRise"			: dict(type=int),
1636	"openTypeHheaCaretSlopeRun"				: dict(type=int),
1637	"openTypeHheaCaretOffset"				: dict(type=(int, float)),
1638	"openTypeNameDesigner"					: dict(type=basestring),
1639	"openTypeNameDesignerURL"				: dict(type=basestring),
1640	"openTypeNameManufacturer"				: dict(type=basestring),
1641	"openTypeNameManufacturerURL"			: dict(type=basestring),
1642	"openTypeNameLicense"					: dict(type=basestring),
1643	"openTypeNameLicenseURL"				: dict(type=basestring),
1644	"openTypeNameVersion"					: dict(type=basestring),
1645	"openTypeNameUniqueID"					: dict(type=basestring),
1646	"openTypeNameDescription"				: dict(type=basestring),
1647	"openTypeNamePreferredFamilyName"		: dict(type=basestring),
1648	"openTypeNamePreferredSubfamilyName"	: dict(type=basestring),
1649	"openTypeNameCompatibleFullName"		: dict(type=basestring),
1650	"openTypeNameSampleText"				: dict(type=basestring),
1651	"openTypeNameWWSFamilyName"				: dict(type=basestring),
1652	"openTypeNameWWSSubfamilyName"			: dict(type=basestring),
1653	"openTypeOS2WidthClass"					: dict(type=int, valueValidator=fontInfoOpenTypeOS2WidthClassValidator),
1654	"openTypeOS2WeightClass"				: dict(type=int, valueValidator=fontInfoOpenTypeOS2WeightClassValidator),
1655	"openTypeOS2Selection"					: dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2SelectionOptions),
1656	"openTypeOS2VendorID"					: dict(type=basestring),
1657	"openTypeOS2Panose"						: dict(type="integerList", valueValidator=fontInfoVersion2OpenTypeOS2PanoseValidator),
1658	"openTypeOS2FamilyClass"				: dict(type="integerList", valueValidator=fontInfoOpenTypeOS2FamilyClassValidator),
1659	"openTypeOS2UnicodeRanges"				: dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2UnicodeRangesOptions),
1660	"openTypeOS2CodePageRanges"				: dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2CodePageRangesOptions),
1661	"openTypeOS2TypoAscender"				: dict(type=(int, float)),
1662	"openTypeOS2TypoDescender"				: dict(type=(int, float)),
1663	"openTypeOS2TypoLineGap"				: dict(type=(int, float)),
1664	"openTypeOS2WinAscent"					: dict(type=(int, float)),
1665	"openTypeOS2WinDescent"					: dict(type=(int, float)),
1666	"openTypeOS2Type"						: dict(type="integerList", valueValidator=genericIntListValidator, valueOptions=fontInfoOpenTypeOS2TypeOptions),
1667	"openTypeOS2SubscriptXSize"				: dict(type=(int, float)),
1668	"openTypeOS2SubscriptYSize"				: dict(type=(int, float)),
1669	"openTypeOS2SubscriptXOffset"			: dict(type=(int, float)),
1670	"openTypeOS2SubscriptYOffset"			: dict(type=(int, float)),
1671	"openTypeOS2SuperscriptXSize"			: dict(type=(int, float)),
1672	"openTypeOS2SuperscriptYSize"			: dict(type=(int, float)),
1673	"openTypeOS2SuperscriptXOffset"			: dict(type=(int, float)),
1674	"openTypeOS2SuperscriptYOffset"			: dict(type=(int, float)),
1675	"openTypeOS2StrikeoutSize"				: dict(type=(int, float)),
1676	"openTypeOS2StrikeoutPosition"			: dict(type=(int, float)),
1677	"openTypeVheaVertTypoAscender"			: dict(type=(int, float)),
1678	"openTypeVheaVertTypoDescender"			: dict(type=(int, float)),
1679	"openTypeVheaVertTypoLineGap"			: dict(type=(int, float)),
1680	"openTypeVheaCaretSlopeRise"			: dict(type=int),
1681	"openTypeVheaCaretSlopeRun"				: dict(type=int),
1682	"openTypeVheaCaretOffset"				: dict(type=(int, float)),
1683	"postscriptFontName"					: dict(type=basestring),
1684	"postscriptFullName"					: dict(type=basestring),
1685	"postscriptSlantAngle"					: dict(type=(float, int)),
1686	"postscriptUniqueID"					: dict(type=int),
1687	"postscriptUnderlineThickness"			: dict(type=(int, float)),
1688	"postscriptUnderlinePosition"			: dict(type=(int, float)),
1689	"postscriptIsFixedPitch"				: dict(type=bool),
1690	"postscriptBlueValues"					: dict(type="integerList", valueValidator=fontInfoPostscriptBluesValidator),
1691	"postscriptOtherBlues"					: dict(type="integerList", valueValidator=fontInfoPostscriptOtherBluesValidator),
1692	"postscriptFamilyBlues"					: dict(type="integerList", valueValidator=fontInfoPostscriptBluesValidator),
1693	"postscriptFamilyOtherBlues"			: dict(type="integerList", valueValidator=fontInfoPostscriptOtherBluesValidator),
1694	"postscriptStemSnapH"					: dict(type="integerList", valueValidator=fontInfoPostscriptStemsValidator),
1695	"postscriptStemSnapV"					: dict(type="integerList", valueValidator=fontInfoPostscriptStemsValidator),
1696	"postscriptBlueFuzz"					: dict(type=(int, float)),
1697	"postscriptBlueShift"					: dict(type=(int, float)),
1698	"postscriptBlueScale"					: dict(type=(float, int)),
1699	"postscriptForceBold"					: dict(type=bool),
1700	"postscriptDefaultWidthX"				: dict(type=(int, float)),
1701	"postscriptNominalWidthX"				: dict(type=(int, float)),
1702	"postscriptWeightName"					: dict(type=basestring),
1703	"postscriptDefaultCharacter"			: dict(type=basestring),
1704	"postscriptWindowsCharacterSet"			: dict(type=int, valueValidator=fontInfoPostscriptWindowsCharacterSetValidator),
1705	"macintoshFONDFamilyID"					: dict(type=int),
1706	"macintoshFONDName"						: dict(type=basestring),
1707}
1708fontInfoAttributesVersion2 = set(fontInfoAttributesVersion2ValueData.keys())
1709
1710fontInfoAttributesVersion3ValueData = deepcopy(fontInfoAttributesVersion2ValueData)
1711fontInfoAttributesVersion3ValueData.update({
1712	"versionMinor"							: dict(type=int, valueValidator=genericNonNegativeIntValidator),
1713	"unitsPerEm"							: dict(type=(int, float), valueValidator=genericNonNegativeNumberValidator),
1714	"openTypeHeadLowestRecPPEM"				: dict(type=int, valueValidator=genericNonNegativeNumberValidator),
1715	"openTypeHheaAscender"					: dict(type=int),
1716	"openTypeHheaDescender"					: dict(type=int),
1717	"openTypeHheaLineGap"					: dict(type=int),
1718	"openTypeHheaCaretOffset"				: dict(type=int),
1719	"openTypeOS2Panose"						: dict(type="integerList", valueValidator=fontInfoVersion3OpenTypeOS2PanoseValidator),
1720	"openTypeOS2TypoAscender"				: dict(type=int),
1721	"openTypeOS2TypoDescender"				: dict(type=int),
1722	"openTypeOS2TypoLineGap"				: dict(type=int),
1723	"openTypeOS2WinAscent"					: dict(type=int, valueValidator=genericNonNegativeNumberValidator),
1724	"openTypeOS2WinDescent"					: dict(type=int, valueValidator=genericNonNegativeNumberValidator),
1725	"openTypeOS2SubscriptXSize"				: dict(type=int),
1726	"openTypeOS2SubscriptYSize"				: dict(type=int),
1727	"openTypeOS2SubscriptXOffset"			: dict(type=int),
1728	"openTypeOS2SubscriptYOffset"			: dict(type=int),
1729	"openTypeOS2SuperscriptXSize"			: dict(type=int),
1730	"openTypeOS2SuperscriptYSize"			: dict(type=int),
1731	"openTypeOS2SuperscriptXOffset"			: dict(type=int),
1732	"openTypeOS2SuperscriptYOffset"			: dict(type=int),
1733	"openTypeOS2StrikeoutSize"				: dict(type=int),
1734	"openTypeOS2StrikeoutPosition"			: dict(type=int),
1735	"openTypeGaspRangeRecords"				: dict(type="dictList", valueValidator=fontInfoOpenTypeGaspRangeRecordsValidator),
1736	"openTypeNameRecords"					: dict(type="dictList", valueValidator=fontInfoOpenTypeNameRecordsValidator),
1737	"openTypeVheaVertTypoAscender"			: dict(type=int),
1738	"openTypeVheaVertTypoDescender"			: dict(type=int),
1739	"openTypeVheaVertTypoLineGap"			: dict(type=int),
1740	"openTypeVheaCaretOffset"				: dict(type=int),
1741	"woffMajorVersion"						: dict(type=int, valueValidator=genericNonNegativeIntValidator),
1742	"woffMinorVersion"						: dict(type=int, valueValidator=genericNonNegativeIntValidator),
1743	"woffMetadataUniqueID"					: dict(type=dict, valueValidator=fontInfoWOFFMetadataUniqueIDValidator),
1744	"woffMetadataVendor"					: dict(type=dict, valueValidator=fontInfoWOFFMetadataVendorValidator),
1745	"woffMetadataCredits"					: dict(type=dict, valueValidator=fontInfoWOFFMetadataCreditsValidator),
1746	"woffMetadataDescription"				: dict(type=dict, valueValidator=fontInfoWOFFMetadataDescriptionValidator),
1747	"woffMetadataLicense"					: dict(type=dict, valueValidator=fontInfoWOFFMetadataLicenseValidator),
1748	"woffMetadataCopyright"					: dict(type=dict, valueValidator=fontInfoWOFFMetadataCopyrightValidator),
1749	"woffMetadataTrademark"					: dict(type=dict, valueValidator=fontInfoWOFFMetadataTrademarkValidator),
1750	"woffMetadataLicensee"					: dict(type=dict, valueValidator=fontInfoWOFFMetadataLicenseeValidator),
1751	"woffMetadataExtensions"				: dict(type=list, valueValidator=fontInfoWOFFMetadataExtensionsValidator),
1752	"guidelines"							: dict(type=list, valueValidator=guidelinesValidator)
1753})
1754fontInfoAttributesVersion3 = set(fontInfoAttributesVersion3ValueData.keys())
1755
1756# insert the type validator for all attrs that
1757# have no defined validator.
1758for attr, dataDict in list(fontInfoAttributesVersion2ValueData.items()):
1759	if "valueValidator" not in dataDict:
1760		dataDict["valueValidator"] = genericTypeValidator
1761
1762for attr, dataDict in list(fontInfoAttributesVersion3ValueData.items()):
1763	if "valueValidator" not in dataDict:
1764		dataDict["valueValidator"] = genericTypeValidator
1765
1766# Version Conversion Support
1767# These are used from converting from version 1
1768# to version 2 or vice-versa.
1769
1770def _flipDict(d):
1771	flipped = {}
1772	for key, value in list(d.items()):
1773		flipped[value] = key
1774	return flipped
1775
1776fontInfoAttributesVersion1To2 = {
1777	"menuName"		: "styleMapFamilyName",
1778	"designer"		: "openTypeNameDesigner",
1779	"designerURL"	: "openTypeNameDesignerURL",
1780	"createdBy"		: "openTypeNameManufacturer",
1781	"vendorURL"		: "openTypeNameManufacturerURL",
1782	"license"		: "openTypeNameLicense",
1783	"licenseURL"	: "openTypeNameLicenseURL",
1784	"ttVersion"		: "openTypeNameVersion",
1785	"ttUniqueID"	: "openTypeNameUniqueID",
1786	"notice"		: "openTypeNameDescription",
1787	"otFamilyName"	: "openTypeNamePreferredFamilyName",
1788	"otStyleName"	: "openTypeNamePreferredSubfamilyName",
1789	"otMacName"		: "openTypeNameCompatibleFullName",
1790	"weightName"	: "postscriptWeightName",
1791	"weightValue"	: "openTypeOS2WeightClass",
1792	"ttVendor"		: "openTypeOS2VendorID",
1793	"uniqueID"		: "postscriptUniqueID",
1794	"fontName"		: "postscriptFontName",
1795	"fondID"		: "macintoshFONDFamilyID",
1796	"fondName"		: "macintoshFONDName",
1797	"defaultWidth"	: "postscriptDefaultWidthX",
1798	"slantAngle"	: "postscriptSlantAngle",
1799	"fullName"		: "postscriptFullName",
1800	# require special value conversion
1801	"fontStyle"		: "styleMapStyleName",
1802	"widthName"		: "openTypeOS2WidthClass",
1803	"msCharSet"		: "postscriptWindowsCharacterSet"
1804}
1805fontInfoAttributesVersion2To1 = _flipDict(fontInfoAttributesVersion1To2)
1806deprecatedFontInfoAttributesVersion2 = set(fontInfoAttributesVersion1To2.keys())
1807
1808_fontStyle1To2 = {
1809	64 : "regular",
1810	1  : "italic",
1811	32 : "bold",
1812	33 : "bold italic"
1813}
1814_fontStyle2To1 = _flipDict(_fontStyle1To2)
1815# Some UFO 1 files have 0
1816_fontStyle1To2[0] = "regular"
1817
1818_widthName1To2 = {
1819	"Ultra-condensed" : 1,
1820	"Extra-condensed" : 2,
1821	"Condensed"		  : 3,
1822	"Semi-condensed"  : 4,
1823	"Medium (normal)" : 5,
1824	"Semi-expanded"	  : 6,
1825	"Expanded"		  : 7,
1826	"Extra-expanded"  : 8,
1827	"Ultra-expanded"  : 9
1828}
1829_widthName2To1 = _flipDict(_widthName1To2)
1830# FontLab's default width value is "Normal".
1831# Many format version 1 UFOs will have this.
1832_widthName1To2["Normal"] = 5
1833# FontLab has an "All" width value. In UFO 1
1834# move this up to "Normal".
1835_widthName1To2["All"] = 5
1836# "medium" appears in a lot of UFO 1 files.
1837_widthName1To2["medium"] = 5
1838# "Medium" appears in a lot of UFO 1 files.
1839_widthName1To2["Medium"] = 5
1840
1841_msCharSet1To2 = {
1842	0	: 1,
1843	1	: 2,
1844	2	: 3,
1845	77	: 4,
1846	128 : 5,
1847	129 : 6,
1848	130 : 7,
1849	134 : 8,
1850	136 : 9,
1851	161 : 10,
1852	162 : 11,
1853	163 : 12,
1854	177 : 13,
1855	178 : 14,
1856	186 : 15,
1857	200 : 16,
1858	204 : 17,
1859	222 : 18,
1860	238 : 19,
1861	255 : 20
1862}
1863_msCharSet2To1 = _flipDict(_msCharSet1To2)
1864
1865# 1 <-> 2
1866
1867def convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value):
1868	"""
1869	Convert value from version 1 to version 2 format.
1870	Returns the new attribute name and the converted value.
1871	If the value is None, None will be returned for the new value.
1872	"""
1873	# convert floats to ints if possible
1874	if isinstance(value, float):
1875		if int(value) == value:
1876			value = int(value)
1877	if value is not None:
1878		if attr == "fontStyle":
1879			v = _fontStyle1To2.get(value)
1880			if v is None:
1881				raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
1882			value = v
1883		elif attr == "widthName":
1884			v = _widthName1To2.get(value)
1885			if v is None:
1886				raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
1887			value = v
1888		elif attr == "msCharSet":
1889			v = _msCharSet1To2.get(value)
1890			if v is None:
1891				raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), attr))
1892			value = v
1893	attr = fontInfoAttributesVersion1To2.get(attr, attr)
1894	return attr, value
1895
1896def convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value):
1897	"""
1898	Convert value from version 2 to version 1 format.
1899	Returns the new attribute name and the converted value.
1900	If the value is None, None will be returned for the new value.
1901	"""
1902	if value is not None:
1903		if attr == "styleMapStyleName":
1904			value = _fontStyle2To1.get(value)
1905		elif attr == "openTypeOS2WidthClass":
1906			value = _widthName2To1.get(value)
1907		elif attr == "postscriptWindowsCharacterSet":
1908			value = _msCharSet2To1.get(value)
1909	attr = fontInfoAttributesVersion2To1.get(attr, attr)
1910	return attr, value
1911
1912def _convertFontInfoDataVersion1ToVersion2(data):
1913	converted = {}
1914	for attr, value in list(data.items()):
1915		# FontLab gives -1 for the weightValue
1916		# for fonts wil no defined value. Many
1917		# format version 1 UFOs will have this.
1918		if attr == "weightValue" and value == -1:
1919			continue
1920		newAttr, newValue = convertFontInfoValueForAttributeFromVersion1ToVersion2(attr, value)
1921		# skip if the attribute is not part of version 2
1922		if newAttr not in fontInfoAttributesVersion2:
1923			continue
1924		# catch values that can't be converted
1925		if value is None:
1926			raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), newAttr))
1927		# store
1928		converted[newAttr] = newValue
1929	return converted
1930
1931def _convertFontInfoDataVersion2ToVersion1(data):
1932	converted = {}
1933	for attr, value in list(data.items()):
1934		newAttr, newValue = convertFontInfoValueForAttributeFromVersion2ToVersion1(attr, value)
1935		# only take attributes that are registered for version 1
1936		if newAttr not in fontInfoAttributesVersion1:
1937			continue
1938		# catch values that can't be converted
1939		if value is None:
1940			raise UFOLibError("Cannot convert value (%s) for attribute %s." % (repr(value), newAttr))
1941		# store
1942		converted[newAttr] = newValue
1943	return converted
1944
1945# 2 <-> 3
1946
1947_ufo2To3NonNegativeInt = set((
1948	"versionMinor",
1949	"openTypeHeadLowestRecPPEM",
1950	"openTypeOS2WinAscent",
1951	"openTypeOS2WinDescent"
1952))
1953_ufo2To3NonNegativeIntOrFloat = set((
1954	"unitsPerEm"
1955))
1956_ufo2To3FloatToInt = set(((
1957	"openTypeHeadLowestRecPPEM",
1958	"openTypeHheaAscender",
1959	"openTypeHheaDescender",
1960	"openTypeHheaLineGap",
1961	"openTypeHheaCaretOffset",
1962	"openTypeOS2TypoAscender",
1963	"openTypeOS2TypoDescender",
1964	"openTypeOS2TypoLineGap",
1965	"openTypeOS2WinAscent",
1966	"openTypeOS2WinDescent",
1967	"openTypeOS2SubscriptXSize",
1968	"openTypeOS2SubscriptYSize",
1969	"openTypeOS2SubscriptXOffset",
1970	"openTypeOS2SubscriptYOffset",
1971	"openTypeOS2SuperscriptXSize",
1972	"openTypeOS2SuperscriptYSize",
1973	"openTypeOS2SuperscriptXOffset",
1974	"openTypeOS2SuperscriptYOffset",
1975	"openTypeOS2StrikeoutSize",
1976	"openTypeOS2StrikeoutPosition",
1977	"openTypeVheaVertTypoAscender",
1978	"openTypeVheaVertTypoDescender",
1979	"openTypeVheaVertTypoLineGap",
1980	"openTypeVheaCaretOffset"
1981)))
1982
1983def convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value):
1984	"""
1985	Convert value from version 2 to version 3 format.
1986	Returns the new attribute name and the converted value.
1987	If the value is None, None will be returned for the new value.
1988	"""
1989	if attr in _ufo2To3FloatToInt:
1990		try:
1991			v = int(round(value))
1992		except (ValueError, TypeError):
1993			raise UFOLibError("Could not convert value for %s." % attr)
1994		if v != value:
1995			value = v
1996	if attr in _ufo2To3NonNegativeInt:
1997		try:
1998			v = int(abs(value))
1999		except (ValueError, TypeError):
2000			raise UFOLibError("Could not convert value for %s." % attr)
2001		if v != value:
2002			value = v
2003	elif attr in _ufo2To3NonNegativeIntOrFloat:
2004		try:
2005			v = float(abs(value))
2006		except (ValueError, TypeError):
2007			raise UFOLibError("Could not convert value for %s." % attr)
2008		if v == int(v):
2009			v = int(v)
2010		if v != value:
2011			value = v
2012	return attr, value
2013
2014def convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value):
2015	"""
2016	Convert value from version 3 to version 2 format.
2017	Returns the new attribute name and the converted value.
2018	If the value is None, None will be returned for the new value.
2019	"""
2020	return attr, value
2021
2022def _convertFontInfoDataVersion3ToVersion2(data):
2023	converted = {}
2024	for attr, value in list(data.items()):
2025		newAttr, newValue = convertFontInfoValueForAttributeFromVersion3ToVersion2(attr, value)
2026		if newAttr not in fontInfoAttributesVersion2:
2027			continue
2028		converted[newAttr] = newValue
2029	return converted
2030
2031def _convertFontInfoDataVersion2ToVersion3(data):
2032	converted = {}
2033	for attr, value in list(data.items()):
2034		attr, value = convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value)
2035		converted[attr] = value
2036	return converted
2037
2038if __name__ == "__main__":
2039	import doctest
2040	doctest.testmod()
2041