2011-10-01 07:51:10 +00:00
"""
2008-01-07 17:40:34 +00:00
A library for importing . ufo files and their descendants .
2009-02-28 15:47:24 +00:00
Refer to http : / / unifiedfontobject . com for the UFO specification .
The UFOReader and UFOWriter classes support versions 1 and 2
of the specification . Up and down conversion functions are also
supplied in this library . These conversion functions are only
necessary if conversion without loading the UFO data into
a set of objects is desired . These functions are :
convertUFOFormatVersion1ToFormatVersion2
Two sets that list the font info attribute names for the two
fontinfo . plist formats are available for external use . These are :
fontInfoAttributesVersion1
fontInfoAttributesVersion2
A set listing the fontinfo . plist attributes that were deprecated
in version 2 is available for external use :
deprecatedFontInfoAttributesVersion2
A function , validateFontInfoVersion2ValueForAttribute , that does
some basic validation on values for a fontinfo . plist value is
available for external use .
Two value conversion functions are availble for converting
fontinfo . plist values between the possible format versions .
convertFontInfoValueForAttributeFromVersion1ToVersion2
convertFontInfoValueForAttributeFromVersion2ToVersion1
2008-01-07 17:40:34 +00:00
"""
import os
2009-02-28 15:47:24 +00:00
import shutil
2008-01-07 17:40:34 +00:00
from cStringIO import StringIO
2011-09-12 17:49:34 +00:00
import codecs
2011-09-12 20:56:12 +00:00
from copy import deepcopy
2011-09-18 12:16:25 +00:00
from plistlib import readPlist , writePlist
from glifLib import GlyphSet , READ_MODE , WRITE_MODE
2011-09-18 12:24:29 +00:00
from validators import *
2011-09-19 01:40:21 +00:00
from filenames import userNameToFileName
2011-09-27 17:58:45 +00:00
from converters import convertUFO1OrUFO2KerningToUFO3Kerning
2008-01-07 17:40:34 +00:00
2009-02-28 15:47:24 +00:00
try :
set
except NameError :
from sets import Set as set
2008-01-07 17:40:34 +00:00
2009-02-28 15:47:24 +00:00
__all__ = [
" makeUFOPath "
" UFOLibError " ,
" UFOReader " ,
" UFOWriter " ,
" convertUFOFormatVersion1ToFormatVersion2 " ,
" fontInfoAttributesVersion1 " ,
" fontInfoAttributesVersion2 " ,
" deprecatedFontInfoAttributesVersion2 " ,
" validateFontInfoVersion2ValueForAttribute " ,
" convertFontInfoValueForAttributeFromVersion1ToVersion2 " ,
" convertFontInfoValueForAttributeFromVersion2ToVersion1 "
2008-01-07 17:40:34 +00:00
]
2009-02-28 15:47:24 +00:00
class UFOLibError ( Exception ) : pass
# ----------
# File Names
# ----------
2011-09-11 23:47:21 +00:00
DEFAULT_GLYPHS_DIRNAME = " glyphs "
2011-09-12 11:35:57 +00:00
DATA_DIRNAME = " data "
IMAGES_DIRNAME = " images "
2009-02-28 15:47:24 +00:00
METAINFO_FILENAME = " metainfo.plist "
FONTINFO_FILENAME = " fontinfo.plist "
LIB_FILENAME = " lib.plist "
GROUPS_FILENAME = " groups.plist "
KERNING_FILENAME = " kerning.plist "
FEATURES_FILENAME = " features.fea "
2011-09-11 23:47:21 +00:00
LAYERCONTENTS_FILENAME = " layercontents.plist "
2011-09-12 13:25:24 +00:00
LAYERINFO_FILENAME = " layerinfo.plist "
2009-02-28 15:47:24 +00:00
2011-09-27 16:18:42 +00:00
DEFAULT_LAYER_NAME = " public.default "
2009-02-28 15:47:24 +00:00
2011-09-19 01:40:21 +00:00
supportedUFOFormatVersions = [ 1 , 2 , 3 ]
2009-02-28 15:47:24 +00:00
# ----------
# UFO Reader
# ----------
2008-01-07 17:40:34 +00:00
class UFOReader ( object ) :
2009-02-28 15:47:24 +00:00
""" Read the various components of the .ufo. """
2008-01-07 17:40:34 +00:00
def __init__ ( self , path ) :
2011-09-19 01:40:21 +00:00
if not os . path . exists ( path ) :
raise UFOLibError ( " The specified UFO doesn ' t exist. " )
2008-01-07 17:40:34 +00:00
self . _path = path
2009-02-28 15:47:24 +00:00
self . readMetaInfo ( )
2011-09-27 17:58:45 +00:00
self . _upConvertedKerningData = None
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# properties
2009-02-28 15:47:24 +00:00
def _get_formatVersion ( self ) :
return self . _formatVersion
formatVersion = property ( _get_formatVersion , doc = " The format version of the UFO. This is determined by reading metainfo.plist during __init__. " )
2011-09-27 17:58:45 +00:00
# up conversion
def _upConvertKerning ( self ) :
"""
2011-10-03 14:43:17 +00:00
Up convert kerning and groups in UFO 1 and 2.
2011-09-27 17:58:45 +00:00
The data will be held internally until each bit of data
2011-10-03 14:43:17 +00:00
has been retrieved . The conversion of both must be done
at once , so the raw data is cached and an error is raised
if one bit of data becomes obsolete before it is called .
2011-09-27 17:58:45 +00:00
"""
if self . _upConvertedKerningData :
testKerning = self . _readKerning ( )
if testKerning != self . _upConvertedKerningData [ " originalKerning " ] :
raise UFOLibError ( " The data in kerning.plist has been modified since it was converted to UFO 3 format. " )
testGroups = self . _readGroups ( )
if testGroups != self . _upConvertedKerningData [ " originalGroups " ] :
raise UFOLibError ( " The data in groups.plist has been modified since it was converted to UFO 3 format. " )
else :
2011-10-03 15:17:31 +00:00
groups = self . _readGroups ( )
invalidFormatMessage = " groups.plist is not properly formatted. "
2011-10-04 17:14:27 +00:00
if not isinstance ( groups , dict ) :
2011-10-03 15:17:31 +00:00
raise UFOLibError ( invalidFormatMessage )
2011-10-04 17:14:27 +00:00
for groupName , glyphList in groups . items ( ) :
2011-10-03 15:17:31 +00:00
if not isinstance ( groupName , basestring ) :
raise UFOLibError ( invalidFormatMessage )
elif not isinstance ( glyphList , list ) :
raise UFOLibError ( invalidFormatMessage )
for glyphName in glyphList :
if not isinstance ( glyphName , basestring ) :
raise UFOLibError ( invalidFormatMessage )
2011-09-27 17:58:45 +00:00
self . _upConvertedKerningData = dict (
kerning = { } ,
originalKerning = self . _readKerning ( ) ,
groups = { } ,
2011-10-03 15:17:31 +00:00
originalGroups = groups
2011-09-27 17:58:45 +00:00
)
# convert kerning and groups
kerning , groups = convertUFO1OrUFO2KerningToUFO3Kerning (
self . _upConvertedKerningData [ " originalKerning " ] ,
2011-10-03 14:43:17 +00:00
deepcopy ( self . _upConvertedKerningData [ " originalGroups " ] )
2011-09-27 17:58:45 +00:00
)
# store
self . _upConvertedKerningData [ " kerning " ] = kerning
self . _upConvertedKerningData [ " groups " ] = groups
2011-09-12 17:49:34 +00:00
# support methods
2008-01-07 17:40:34 +00:00
def _checkForFile ( self , path ) :
if not os . path . exists ( path ) :
return False
else :
return True
2009-02-28 15:47:24 +00:00
2011-09-26 22:16:08 +00:00
def _readPlist ( self , path ) :
"""
Read a property list . The errors that
could be raised during the reading of
a plist are unpredictable and / or too
large to list , so , a blind try : except :
is done . If an exception occurs , a
UFOLibError will be raised .
"""
originalPath = path
path = os . path . join ( self . _path , path )
try :
data = readPlist ( path )
return data
except :
raise UFOLibError ( " The file %s could not be read. " % originalPath )
2011-09-12 17:49:34 +00:00
def readBytesFromPath ( self , path , encoding = None ) :
2011-09-12 11:35:57 +00:00
"""
2011-09-12 17:49:34 +00:00
Returns the bytes in the file at the given path .
2011-09-12 11:35:57 +00:00
The path must be relative to the UFO path .
Returns None if the file does not exist .
2011-09-12 17:49:34 +00:00
An encoding may be passed if needed .
2011-09-12 11:35:57 +00:00
"""
2011-10-19 16:22:08 +00:00
fullPath = os . path . join ( self . _path , path )
if not self . _checkForFile ( fullPath ) :
2011-09-12 11:35:57 +00:00
return None
2011-10-19 16:39:49 +00:00
if os . path . isdir ( fullPath ) :
2011-10-19 16:22:08 +00:00
raise UFOLibError ( " %s is a directory. " % path )
f = codecs . open ( fullPath , READ_MODE , encoding = encoding )
2011-09-12 11:35:57 +00:00
data = f . read ( )
f . close ( )
return data
2011-09-12 17:49:34 +00:00
def getReadFileForPath ( self , path , encoding = None ) :
"""
Returns a file ( or file - like ) object for the
file at the given path . The path must be relative
to the UFO path . Returns None if the file does not exist .
An encoding may be passed if needed .
Note : The caller is responsible for closing the open file .
"""
2011-10-19 16:22:08 +00:00
fullPath = os . path . join ( self . _path , path )
if not self . _checkForFile ( fullPath ) :
2011-09-12 17:49:34 +00:00
return None
2011-10-19 16:39:49 +00:00
if os . path . isdir ( fullPath ) :
2011-10-19 16:22:08 +00:00
raise UFOLibError ( " %s is a directory. " % path )
2011-12-06 18:47:56 +00:00
f = codecs . open ( fullPath , READ_MODE , encoding = encoding )
2011-09-12 17:49:34 +00:00
return f
2011-10-28 16:54:11 +00:00
def getFileModificationTime ( self , path ) :
2011-10-27 14:53:54 +00:00
"""
2011-10-28 16:54:11 +00:00
Returns the modification time ( as reported by os . path . getmtime )
2011-10-27 14:53:54 +00:00
for the file at the given path . The path must be relative to
the UFO path . Returns None if the file does not exist .
"""
fullPath = os . path . join ( self . _path , path )
if not self . _checkForFile ( fullPath ) :
return None
2011-10-28 16:54:11 +00:00
return os . path . getmtime ( fullPath )
2011-10-27 14:53:54 +00:00
2011-09-12 17:49:34 +00:00
# metainfo.plist
2008-01-07 17:40:34 +00:00
def readMetaInfo ( self ) :
2009-02-28 15:47:24 +00:00
"""
Read metainfo . plist . Only used for internal operations .
"""
2008-01-07 17:40:34 +00:00
path = os . path . join ( self . _path , METAINFO_FILENAME )
if not self . _checkForFile ( path ) :
2009-02-28 15:47:24 +00:00
raise UFOLibError ( " metainfo.plist is missing in %s . This file is required. " % self . _path )
# should there be a blind try/except with a UFOLibError
# raised in except here (and elsewhere)? It would be nice to
# provide external callers with a single exception to catch.
2011-09-28 14:47:25 +00:00
data = self . _readPlist ( path )
if not isinstance ( data , dict ) :
raise UFOLibError ( " maetainfo.plist is not properly formatted. " )
2009-02-28 15:47:24 +00:00
formatVersion = data [ " formatVersion " ]
if formatVersion not in supportedUFOFormatVersions :
raise UFOLibError ( " Unsupported UFO format ( %d ) in %s . " % ( formatVersion , self . _path ) )
self . _formatVersion = formatVersion
2011-09-12 17:49:34 +00:00
# groups.plist
2011-09-27 17:58:45 +00:00
def _readGroups ( self ) :
path = os . path . join ( self . _path , GROUPS_FILENAME )
if not self . _checkForFile ( path ) :
return { }
2011-09-28 14:47:25 +00:00
data = self . _readPlist ( path )
return data
2011-09-27 17:58:45 +00:00
2008-01-07 17:40:34 +00:00
def readGroups ( self ) :
2009-02-28 15:47:24 +00:00
"""
Read groups . plist . Returns a dict .
"""
2011-09-27 17:58:45 +00:00
# handle up conversion
if self . _formatVersion < 3 :
self . _upConvertKerning ( )
2011-10-03 15:17:31 +00:00
groups = self . _upConvertedKerningData [ " groups " ]
2011-09-27 17:58:45 +00:00
# normal
else :
2011-10-03 15:17:31 +00:00
groups = self . _readGroups ( )
2011-10-03 15:56:08 +00:00
valid , message = groupsValidator ( groups )
2011-10-03 15:17:31 +00:00
if not valid :
raise UFOLibError ( message )
return groups
2011-09-27 17:58:45 +00:00
# fontinfo.plist
def _readInfo ( self ) :
path = os . path . join ( self . _path , FONTINFO_FILENAME )
2008-01-07 17:40:34 +00:00
if not self . _checkForFile ( path ) :
return { }
2011-09-28 14:47:25 +00:00
data = self . _readPlist ( path )
if not isinstance ( data , dict ) :
raise UFOLibError ( " fontinfo.plist is not properly formatted. " )
return data
2009-02-28 15:47:24 +00:00
2008-01-07 17:40:34 +00:00
def readInfo ( self , info ) :
2009-02-28 15:47:24 +00:00
"""
Read fontinfo . plist . It requires an object that allows
setting attributes with names that follow the fontinfo . plist
2011-09-27 17:58:45 +00:00
version 3 specification . This will write the attributes
2009-02-28 15:47:24 +00:00
defined in the file into the object .
"""
2011-10-03 14:43:17 +00:00
path = os . path . join ( self . _path , FONTINFO_FILENAME )
if not self . _checkForFile ( path ) :
return { }
infoDict = self . _readPlist ( path )
if not isinstance ( infoDict , dict ) :
raise UFOLibError ( " fontinfo.plist is not properly formatted. " )
2009-02-28 15:47:24 +00:00
infoDataToSet = { }
# version 1
if self . _formatVersion == 1 :
for attr in fontInfoAttributesVersion1 :
value = infoDict . get ( attr )
if value is not None :
infoDataToSet [ attr ] = value
infoDataToSet = _convertFontInfoDataVersion1ToVersion2 ( infoDataToSet )
# version 2
elif self . _formatVersion == 2 :
2011-09-18 12:24:29 +00:00
for attr , dataValidationDict in fontInfoAttributesVersion2ValueData . items ( ) :
2009-02-28 15:47:24 +00:00
value = infoDict . get ( attr )
if value is None :
continue
infoDataToSet [ attr ] = value
2011-09-14 21:13:27 +00:00
# version 3
elif self . _formatVersion == 3 :
2011-09-18 12:24:29 +00:00
for attr , dataValidationDict in fontInfoAttributesVersion3ValueData . items ( ) :
2011-09-14 21:13:27 +00:00
value = infoDict . get ( attr )
if value is None :
continue
infoDataToSet [ attr ] = value
2009-02-28 15:47:24 +00:00
# unsupported version
else :
raise NotImplementedError
# validate data
2011-09-14 21:13:27 +00:00
if self . _formatVersion < 3 :
2011-09-18 12:24:29 +00:00
infoDataToSet = validateInfoVersion2Data ( infoDataToSet )
2011-09-14 21:13:27 +00:00
elif self . _formatVersion == 3 :
2011-09-18 12:24:29 +00:00
infoDataToSet = validateInfoVersion3Data ( infoDataToSet )
2009-02-28 15:47:24 +00:00
# populate the object
for attr , value in infoDataToSet . items ( ) :
2008-01-07 17:40:34 +00:00
try :
2009-02-28 15:47:24 +00:00
setattr ( info , attr , value )
2008-01-07 17:40:34 +00:00
except AttributeError :
2009-02-28 15:47:24 +00:00
raise UFOLibError ( " The supplied info object does not support setting a necessary attribute ( %s ). " % attr )
2011-09-12 17:49:34 +00:00
# kerning.plist
2011-09-27 17:58:45 +00:00
def _readKerning ( self ) :
path = os . path . join ( self . _path , KERNING_FILENAME )
if not self . _checkForFile ( path ) :
return { }
2011-09-28 14:47:25 +00:00
data = self . _readPlist ( path )
2011-09-29 13:31:37 +00:00
invalidFormatMessage = " kerning.plist is not properly formatted. "
2011-09-28 14:47:25 +00:00
if not isinstance ( data , dict ) :
2011-09-29 13:31:37 +00:00
raise UFOLibError ( invalidFormatMessage )
for first , secondDict in data . items ( ) :
if not isinstance ( first , basestring ) :
raise UFOLibError ( invalidFormatMessage )
elif not isinstance ( secondDict , dict ) :
raise UFOLibError ( invalidFormatMessage )
for second , value in secondDict . items ( ) :
if not isinstance ( second , basestring ) :
raise UFOLibError ( invalidFormatMessage )
elif not isinstance ( value , ( int , float ) ) :
raise UFOLibError ( invalidFormatMessage )
2011-09-28 14:47:25 +00:00
return data
2011-09-27 17:58:45 +00:00
2008-01-07 17:40:34 +00:00
def readKerning ( self ) :
2009-02-28 15:47:24 +00:00
"""
Read kerning . plist . Returns a dict .
2011-12-06 18:46:31 +00:00
This performs structural validation of the kerning data ,
but it does not check the validity of the kerning as
dictated in the UFO spec . To do that , pass the kerning
obtained from this method and the groups obtained from
readGroups to the kerningvalidator function in the
validators module .
2009-02-28 15:47:24 +00:00
"""
2011-09-27 17:58:45 +00:00
# handle up conversion
if self . _formatVersion < 3 :
self . _upConvertKerning ( )
kerningNested = self . _upConvertedKerningData [ " kerning " ]
# normal
else :
kerningNested = self . _readKerning ( )
# flatten
2008-01-07 17:40:34 +00:00
kerning = { }
for left in kerningNested :
for right in kerningNested [ left ] :
value = kerningNested [ left ] [ right ]
kerning [ left , right ] = value
return kerning
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# lib.plist
2008-01-07 17:40:34 +00:00
def readLib ( self ) :
2009-02-28 15:47:24 +00:00
"""
Read lib . plist . Returns a dict .
"""
2008-01-07 17:40:34 +00:00
path = os . path . join ( self . _path , LIB_FILENAME )
if not self . _checkForFile ( path ) :
return { }
2011-09-28 14:47:25 +00:00
data = self . _readPlist ( path )
2011-10-17 20:32:20 +00:00
valid , message = fontLibValidator ( data )
2011-10-03 15:56:08 +00:00
if not valid :
raise UFOLibError ( message )
return data
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# features.fea
2009-02-28 15:47:24 +00:00
def readFeatures ( self ) :
"""
Read features . fea . Returns a string .
"""
path = os . path . join ( self . _path , FEATURES_FILENAME )
if not self . _checkForFile ( path ) :
return " "
f = open ( path , READ_MODE )
text = f . read ( )
f . close ( )
return text
2011-09-12 17:49:34 +00:00
# glyph sets & layers
2011-09-26 20:12:19 +00:00
def _readLayerContents ( self ) :
2011-09-11 23:47:21 +00:00
"""
2011-09-26 19:57:10 +00:00
Rebuild the layer contents list by checking what glyphsets
are available on disk .
2011-09-11 23:47:21 +00:00
"""
2011-09-26 19:57:10 +00:00
if self . _formatVersion < 3 :
2011-09-26 22:16:08 +00:00
return [ ( DEFAULT_LAYER_NAME , DEFAULT_GLYPHS_DIRNAME ) ]
2011-09-26 19:57:10 +00:00
# read the file on disk
2011-09-11 23:47:21 +00:00
path = os . path . join ( self . _path , LAYERCONTENTS_FILENAME )
2011-09-26 19:57:10 +00:00
if not os . path . exists ( path ) :
raise UFOLibError ( " layercontents.plist is missing. " )
if os . path . exists ( path ) :
2011-09-26 22:16:08 +00:00
contents = self . _readPlist ( path )
valid , error = layerContentsValidator ( contents , self . _path )
2011-09-26 19:57:10 +00:00
if not valid :
raise UFOLibError ( error )
2011-09-26 22:16:08 +00:00
return contents
2011-09-11 23:47:21 +00:00
def getLayerNames ( self ) :
"""
Get the ordered layer names from layercontents . plist .
"""
layerContents = self . _readLayerContents ( )
layerNames = [ layerName for layerName , directoryName in layerContents ]
return layerNames
def getDefaultLayerName ( self ) :
"""
Get the default layer name from layercontents . plist .
"""
layerContents = self . _readLayerContents ( )
for layerName , layerDirectory in layerContents :
2011-09-26 22:16:08 +00:00
if layerDirectory == DEFAULT_GLYPHS_DIRNAME :
2011-09-11 23:47:21 +00:00
return layerName
2011-09-26 19:57:10 +00:00
# this will already have been raised during __init__
2011-09-11 23:47:21 +00:00
raise UFOLibError ( " The default layer is not defined in layercontents.plist. " )
def getGlyphSet ( self , layerName = None ) :
2009-02-28 15:47:24 +00:00
"""
Return the GlyphSet associated with the
2011-09-11 23:47:21 +00:00
glyphs directory mapped to layerName
in the UFO . If layerName is not provided ,
the name retrieved with getDefaultLayerName
will be used .
2009-02-28 15:47:24 +00:00
"""
2011-09-11 23:47:21 +00:00
if layerName is None :
layerName = self . getDefaultLayerName ( )
directory = None
layerContents = self . _readLayerContents ( )
for storedLayerName , storedLayerDirectory in layerContents :
if layerName == storedLayerName :
directory = storedLayerDirectory
break
if directory is None :
raise UFOLibError ( " No glyphs directory is mapped to \" %s \" . " % layerName )
glyphsPath = os . path . join ( self . _path , directory )
2011-10-07 13:54:14 +00:00
return GlyphSet ( glyphsPath , ufoFormatVersion = self . _formatVersion )
2008-01-07 17:40:34 +00:00
2011-09-30 20:44:21 +00:00
def getCharacterMapping ( self , layerName = None ) :
2009-02-28 15:47:24 +00:00
"""
Return a dictionary that maps unicode values ( ints ) to
2008-01-07 17:40:34 +00:00
lists of glyph names .
"""
2011-12-01 14:11:37 +00:00
glyphSet = self . getGlyphSet ( layerName )
2008-01-07 17:40:34 +00:00
allUnicodes = glyphSet . getUnicodes ( )
cmap = { }
for glyphName , unicodes in allUnicodes . iteritems ( ) :
for code in unicodes :
if code in cmap :
cmap [ code ] . append ( glyphName )
else :
cmap [ code ] = [ glyphName ]
return cmap
2011-09-12 17:49:34 +00:00
# /data
2011-09-12 11:35:57 +00:00
def getDataDirectoryListing ( self , maxDepth = 100 ) :
"""
2011-10-19 16:22:08 +00:00
Returns a list of all files in the data directory .
The returned paths will be relative to the UFO .
This will not list directory names , only file names .
Thus , empty directories will be skipped .
2011-09-12 11:51:24 +00:00
The maxDepth argument sets the maximum number
of sub - directories that are allowed .
2011-09-12 11:35:57 +00:00
"""
path = os . path . join ( self . _path , DATA_DIRNAME )
if not self . _checkForFile ( path ) :
return [ ]
listing = self . _getDirectoryListing ( path , maxDepth = maxDepth )
2011-10-19 16:24:12 +00:00
listing = [ os . path . relpath ( path , " data " ) for path in listing ]
2011-09-12 11:35:57 +00:00
return listing
def _getDirectoryListing ( self , path , depth = 0 , maxDepth = 100 ) :
if depth > maxDepth :
raise UFOLibError ( " Maximum recusion depth reached. " )
result = [ ]
for fileName in os . listdir ( path ) :
p = os . path . join ( path , fileName )
if os . path . isdir ( p ) :
result + = self . _getDirectoryListing ( p , depth = depth + 1 , maxDepth = maxDepth )
else :
p = os . path . relpath ( p , self . _path )
result . append ( p )
return result
2008-01-07 17:40:34 +00:00
2011-10-11 16:19:11 +00:00
def getImageDirectoryListing ( self ) :
"""
Returns a list of all image file names in
the images directory . Each of the images will
have been verified to have the PNG signature .
"""
2011-10-11 16:21:58 +00:00
if self . _formatVersion < 3 :
return [ ]
2011-10-11 16:19:11 +00:00
path = os . path . join ( self . _path , IMAGES_DIRNAME )
if not os . path . exists ( path ) :
return [ ]
if not os . path . isdir ( path ) :
raise UFOLibError ( " The UFO contains an \" images \" file instead of a directory. " )
result = [ ]
for fileName in os . listdir ( path ) :
p = os . path . join ( path , fileName )
if os . path . isdir ( p ) :
# silently skip this as version control
# systems often have hidden directories
continue
valid , error = pngValidator ( path = p )
if valid :
result . append ( fileName )
return result
def readImage ( self , fileName ) :
"""
Return image data for the file named fileName .
"""
2011-10-11 16:21:58 +00:00
if self . _formatVersion < 3 :
raise UFOLibError ( " Reading images is not allowed in UFO %d . " % self . _formatVersion )
2011-10-11 16:19:11 +00:00
data = self . readBytesFromPath ( os . path . join ( IMAGES_DIRNAME , fileName ) )
if data is None :
raise UFOLibError ( " No image file named %s . " % fileName )
valid , error = pngValidator ( data = data )
if not valid :
raise UFOLibError ( error )
return data
2011-09-12 13:25:24 +00:00
2009-02-28 15:47:24 +00:00
# ----------
# UFO Writer
# ----------
2008-01-07 17:40:34 +00:00
class UFOWriter ( object ) :
2009-02-28 15:47:24 +00:00
""" Write the various components of the .ufo. """
2011-09-14 21:13:27 +00:00
def __init__ ( self , path , formatVersion = 3 , fileCreator = " org.robofab.ufoLib " ) :
2009-02-28 15:47:24 +00:00
if formatVersion not in supportedUFOFormatVersions :
raise UFOLibError ( " Unsupported UFO format ( %d ). " % formatVersion )
2011-09-27 15:28:42 +00:00
# establish some basic stuff
self . _path = path
self . _formatVersion = formatVersion
self . _fileCreator = fileCreator
2011-09-26 19:57:10 +00:00
# if the file already exists, get the format version.
# this will be needed for up and down conversion.
previousFormatVersion = None
if os . path . exists ( path ) :
p = os . path . join ( path , METAINFO_FILENAME )
if not os . path . exists ( p ) :
raise UFOLibError ( " The metainfo.plist file is not in the existing UFO. " )
2011-09-27 15:28:42 +00:00
metaInfo = self . _readPlist ( METAINFO_FILENAME )
previousFormatVersion = metaInfo . get ( " formatVersion " )
try :
previousFormatVersion = int ( previousFormatVersion )
except :
raise UFOLibError ( " The existing metainfo.plist is not properly formatted. " )
if previousFormatVersion not in supportedUFOFormatVersions :
raise UFOLibError ( " Unsupported UFO format ( %d ). " % formatVersion )
2011-10-06 13:30:11 +00:00
# catch down conversion
if previousFormatVersion is not None and previousFormatVersion > formatVersion :
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 ) )
2011-09-26 19:57:10 +00:00
# handle the layer contents
2011-09-19 01:40:21 +00:00
self . layerContents = { }
2011-09-26 19:57:10 +00:00
if previousFormatVersion > = 3 :
# already exists
self . _readLayerContents ( )
else :
2011-09-19 01:40:21 +00:00
# previous < 3
# imply the layer contents
2011-09-26 19:57:10 +00:00
p = os . path . join ( path , DEFAULT_GLYPHS_DIRNAME )
if os . path . exists ( p ) :
2011-09-27 15:28:42 +00:00
self . layerContents = { DEFAULT_LAYER_NAME : DEFAULT_GLYPHS_DIRNAME }
2011-09-26 19:57:10 +00:00
# write the new metainfo
2009-02-28 15:47:24 +00:00
self . _writeMetaInfo ( )
2011-09-12 17:49:34 +00:00
# properties
2009-02-28 15:47:24 +00:00
2011-10-27 14:59:01 +00:00
def _get_path ( self ) :
return self . _path
path = property ( _get_path , doc = " The path the UFO is being written to. " )
2009-02-28 15:47:24 +00:00
def _get_formatVersion ( self ) :
return self . _formatVersion
formatVersion = property ( _get_formatVersion , doc = " The format version of the UFO. This is set into metainfo.plist during __init__. " )
def _get_fileCreator ( self ) :
return self . _fileCreator
fileCreator = property ( _get_fileCreator , doc = " The file creator of the UFO. This is set into metainfo.plist during __init__. " )
2011-09-12 17:49:34 +00:00
# support methods
2011-09-19 01:40:21 +00:00
def _readPlist ( self , path ) :
"""
Read a property list . The errors that
could be raised during the reading of
a plist are unpredictable and / or too
large to list , so , a blind try : except :
is done . If an exception occurs , a
UFOLibError will be raised .
"""
originalPath = path
path = os . path . join ( self . _path , path )
try :
data = readPlist ( path )
return data
except :
raise UFOLibError ( " The file %s could not be read. " % originalPath )
def _writePlist ( self , data , path ) :
"""
Write a property list . The errors that
could be raised during the writing of
a plist are unpredictable and / or too
large to list , so , a blind try : except :
is done . If an exception occurs , a
UFOLibError will be raised .
"""
originalPath = path
path = os . path . join ( self . _path , path )
try :
data = writePlistAtomically ( data , path )
except :
raise UFOLibError ( " The data for the file %s could not be written because it is not properly formatted. " % originalPath )
2011-09-12 13:25:24 +00:00
2008-01-07 17:40:34 +00:00
def _makeDirectory ( self , subDirectory = None ) :
path = self . _path
if subDirectory :
path = os . path . join ( self . _path , subDirectory )
if not os . path . exists ( path ) :
os . makedirs ( path )
return path
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
def _buildDirectoryTree ( self , path ) :
directory , fileName = os . path . split ( path )
directoryTree = [ ]
while directory :
directory , d = os . path . split ( directory )
directoryTree . append ( d )
directoryTree . reverse ( )
built = " "
for d in directoryTree :
d = os . path . join ( built , d )
p = os . path . join ( self . _path , d )
if not os . path . exists ( p ) :
os . mkdir ( p )
built = d
2011-09-19 01:40:21 +00:00
def _removeFileForPath ( self , path , raiseErrorIfMissing = False ) :
originalPath = path
path = os . path . join ( self . _path , path )
if not os . path . exists ( path ) :
if raiseErrorIfMissing :
raise UFOLibError ( " The file %s does not exist. " % path )
else :
if os . path . isdir ( path ) :
shutil . rmtree ( path )
else :
os . remove ( path )
# remove any directories that are now empty
self . _removeEmptyDirectoriesForPath ( os . path . dirname ( originalPath ) )
2011-09-12 17:49:34 +00:00
def _removeEmptyDirectoriesForPath ( self , directory ) :
absoluteDirectory = os . path . join ( self . _path , directory )
2011-09-19 01:40:21 +00:00
if not os . path . exists ( absoluteDirectory ) :
return
2011-09-12 17:49:34 +00:00
if not len ( os . listdir ( absoluteDirectory ) ) :
shutil . rmtree ( absoluteDirectory )
else :
return
directory = os . path . dirname ( directory )
if directory :
self . _removeEmptyDirectoriesForPath ( directory )
2011-09-26 19:57:10 +00:00
# file system interaction
2011-09-12 17:49:34 +00:00
def writeBytesToPath ( self , path , bytes , encoding = None ) :
"""
Write bytes to path . If needed , the directory tree
for the given path will be built . The path must be
relative to the UFO . An encoding may be passed if needed .
"""
2011-10-19 16:39:49 +00:00
fullPath = os . path . join ( self . _path , path )
if os . path . exists ( fullPath ) and os . path . isdir ( fullPath ) :
raise UFOLibError ( " A directory exists at %s . " % path )
2011-09-12 17:49:34 +00:00
self . _buildDirectoryTree ( path )
2011-10-19 16:39:49 +00:00
writeFileAtomically ( bytes , fullPath , encoding = encoding )
2011-09-12 17:49:34 +00:00
def getFileObjectForPath ( self , path , encoding = None ) :
"""
Creates a write mode file object at path . If needed ,
the directory tree for the given path will be built .
The path must be relative to the UFO . An encoding may
be passed if needed .
Note : The caller is responsible for closing the open file .
"""
2011-10-19 16:39:49 +00:00
fullPath = os . path . join ( self . _path , path )
if os . path . exists ( fullPath ) and os . path . isdir ( fullPath ) :
raise UFOLibError ( " A directory exists at %s . " % path )
2011-09-12 17:49:34 +00:00
self . _buildDirectoryTree ( path )
2011-12-06 18:47:56 +00:00
return codecs . open ( fullPath , WRITE_MODE , encoding = encoding )
2011-09-12 17:49:34 +00:00
def removeFileForPath ( self , path ) :
"""
Remove the file ( or directory ) at path . The path
must be relative to the UFO . This is only allowed
for files in the data and image directories .
"""
# make sure that only data or images is being changed
d = path
parts = [ ]
while d :
d , p = os . path . split ( d )
if p :
parts . append ( p )
if parts [ - 1 ] not in ( " images " , " data " ) :
raise UFOLibError ( " Removing \" %s \" is not legal. " % path )
# remove the file
2011-09-19 01:40:21 +00:00
self . _removeFileForPath ( path , raiseErrorIfMissing = True )
2011-09-12 17:49:34 +00:00
2011-10-19 02:18:56 +00:00
def copyFromReader ( self , reader , sourcePath , destPath ) :
"""
Copy the sourcePath in the provided UFOReader to destPath
in this writer . The paths must be relative . They may represent
2011-10-19 14:58:35 +00:00
directories or paths . This uses the most memory efficient
2011-10-19 02:18:56 +00:00
method possible for copying the data possible .
"""
if not isinstance ( reader , UFOReader ) :
raise UFOLibError ( " The reader must be an instance of UFOReader. " )
fullSourcePath = os . path . join ( reader . _path , sourcePath )
if not reader . _checkForFile ( fullSourcePath ) :
raise UFOLibError ( " No file named \" %s \" to copy from. " % sourcePath )
fullDestPath = os . path . join ( self . _path , destPath )
2011-10-19 14:58:35 +00:00
if os . path . exists ( fullDestPath ) :
raise UFOLibError ( " A file named \" %s \" already exists. " % sourcePath )
self . _buildDirectoryTree ( destPath )
2011-10-19 02:18:56 +00:00
if os . path . isdir ( fullSourcePath ) :
shutil . copytree ( fullSourcePath , fullDestPath )
else :
shutil . copy ( fullSourcePath , fullDestPath )
2011-09-12 17:49:34 +00:00
# metainfo.plist
2008-01-07 17:40:34 +00:00
def _writeMetaInfo ( self ) :
2009-02-28 15:47:24 +00:00
self . _makeDirectory ( )
2008-01-07 17:40:34 +00:00
path = os . path . join ( self . _path , METAINFO_FILENAME )
2009-02-28 15:47:24 +00:00
metaInfo = dict (
creator = self . _fileCreator ,
formatVersion = self . _formatVersion
)
2011-09-19 01:40:21 +00:00
self . _writePlist ( metaInfo , path )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# groups.plist
2008-01-07 17:40:34 +00:00
def writeGroups ( self , groups ) :
2009-02-28 15:47:24 +00:00
"""
Write groups . plist . This method requires a
dict of glyph groups as an argument .
"""
2011-10-03 15:56:08 +00:00
valid , message = groupsValidator ( groups )
2011-10-03 15:17:31 +00:00
if not valid :
raise UFOLibError ( message )
2008-01-07 17:40:34 +00:00
self . _makeDirectory ( )
path = os . path . join ( self . _path , GROUPS_FILENAME )
groupsNew = { }
for key , value in groups . items ( ) :
groupsNew [ key ] = list ( value )
if groupsNew :
2011-09-19 01:40:21 +00:00
self . _writePlist ( groupsNew , path )
2008-01-07 17:40:34 +00:00
elif os . path . exists ( path ) :
os . remove ( path )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# fontinfo.plist
2008-01-07 17:40:34 +00:00
def writeInfo ( self , info ) :
2009-02-28 15:47:24 +00:00
"""
Write info . plist . This method requires an object
that supports getting attributes that follow the
2011-09-14 21:13:27 +00:00
fontinfo . plist version 2 specification . Attributes
2009-02-28 15:47:24 +00:00
will be taken from the given object and written
into the file .
"""
2008-01-07 17:40:34 +00:00
self . _makeDirectory ( )
path = os . path . join ( self . _path , FONTINFO_FILENAME )
2011-09-14 21:13:27 +00:00
# gather version 3 data
2009-02-28 15:47:24 +00:00
infoData = { }
2011-09-18 12:24:29 +00:00
for attr in fontInfoAttributesVersion3ValueData . keys ( ) :
2011-09-14 21:13:27 +00:00
if hasattr ( info , attr ) :
try :
value = getattr ( info , attr )
except AttributeError :
raise UFOLibError ( " The supplied info object does not support getting a necessary attribute ( %s ). " % attr )
if value is None :
continue
infoData [ attr ] = value
# down convert data if necessary and validate
if self . _formatVersion == 3 :
2011-09-18 12:24:29 +00:00
infoData = validateInfoVersion3Data ( infoData )
2011-09-14 21:13:27 +00:00
elif self . _formatVersion == 2 :
infoData = _convertFontInfoDataVersion3ToVersion2 ( infoData )
2011-09-18 12:24:29 +00:00
infoData = validateInfoVersion2Data ( infoData )
2011-09-14 21:13:27 +00:00
elif self . _formatVersion == 1 :
infoData = _convertFontInfoDataVersion3ToVersion2 ( infoData )
2011-09-18 12:24:29 +00:00
infoData = validateInfoVersion2Data ( infoData )
2009-02-28 15:47:24 +00:00
infoData = _convertFontInfoDataVersion2ToVersion1 ( infoData )
# write file
2011-09-19 01:40:21 +00:00
self . _writePlist ( infoData , path )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# kerning.plist
2008-01-07 17:40:34 +00:00
def writeKerning ( self , kerning ) :
2009-02-28 15:47:24 +00:00
"""
Write kerning . plist . This method requires a
dict of kerning pairs as an argument .
2011-12-06 18:46:31 +00:00
This performs basic structural validation of the kerning ,
but it does not check for compliance with the spec in
regards to conflicting pairs . The assumption is that the
kerning data being passed is standards compliant .
2009-02-28 15:47:24 +00:00
"""
2011-09-29 13:36:45 +00:00
invalidFormatMessage = " The kerning is not properly formatted. "
2011-10-06 19:23:48 +00:00
if not isDictEnough ( kerning ) :
2011-09-29 13:36:45 +00:00
raise UFOLibError ( invalidFormatMessage )
for pair , value in kerning . items ( ) :
if not isinstance ( pair , ( list , tuple ) ) :
raise UFOLibError ( invalidFormatMessage )
if not len ( pair ) == 2 :
raise UFOLibError ( invalidFormatMessage )
if not isinstance ( pair [ 0 ] , basestring ) :
raise UFOLibError ( invalidFormatMessage )
if not isinstance ( pair [ 1 ] , basestring ) :
raise UFOLibError ( invalidFormatMessage )
if not isinstance ( value , ( int , float ) ) :
raise UFOLibError ( invalidFormatMessage )
2008-01-07 17:40:34 +00:00
self . _makeDirectory ( )
path = os . path . join ( self . _path , KERNING_FILENAME )
kerningDict = { }
for left , right in kerning . keys ( ) :
value = kerning [ left , right ]
if not left in kerningDict :
kerningDict [ left ] = { }
kerningDict [ left ] [ right ] = value
if kerningDict :
2011-09-19 01:40:21 +00:00
self . _writePlist ( kerningDict , path )
2008-01-07 17:40:34 +00:00
elif os . path . exists ( path ) :
os . remove ( path )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# lib.plist
2008-01-07 17:40:34 +00:00
def writeLib ( self , libDict ) :
2009-02-28 15:47:24 +00:00
"""
Write lib . plist . This method requires a
lib dict as an argument .
"""
2011-10-17 20:32:20 +00:00
valid , message = fontLibValidator ( libDict )
2011-10-03 15:56:08 +00:00
if not valid :
raise UFOLibError ( message )
2008-01-07 17:40:34 +00:00
self . _makeDirectory ( )
path = os . path . join ( self . _path , LIB_FILENAME )
if libDict :
2011-09-19 01:40:21 +00:00
self . _writePlist ( libDict , path )
2008-01-07 17:40:34 +00:00
elif os . path . exists ( path ) :
os . remove ( path )
2011-09-12 17:49:34 +00:00
# features.fea
2009-02-28 15:47:24 +00:00
def writeFeatures ( self , features ) :
"""
Write features . fea . This method requires a
features string as an argument .
"""
if self . _formatVersion == 1 :
raise UFOLibError ( " features.fea is not allowed in UFO Format Version 1. " )
2011-09-29 13:36:45 +00:00
if not isinstance ( features , basestring ) :
raise UFOLibError ( " The features are not text. " )
2009-02-28 15:47:24 +00:00
self . _makeDirectory ( )
path = os . path . join ( self . _path , FEATURES_FILENAME )
writeFileAtomically ( features , path )
2011-09-12 17:49:34 +00:00
# glyph sets & layers
2011-09-19 01:40:21 +00:00
def _readLayerContents ( self ) :
2009-02-28 15:47:24 +00:00
"""
2011-09-27 15:28:42 +00:00
Rebuild the layer contents list by checking what glyph sets
2011-09-19 01:40:21 +00:00
are available on disk .
2009-02-28 15:47:24 +00:00
"""
2011-09-19 01:40:21 +00:00
# read the file on disk
path = os . path . join ( self . _path , LAYERCONTENTS_FILENAME )
if not os . path . exists ( path ) :
raise UFOLibError ( " layercontents.plist is missing. " )
contents = { }
if os . path . exists ( path ) :
raw = self . _readPlist ( path )
valid , error = layerContentsValidator ( raw , self . _path )
if not valid :
raise UFOLibError ( error )
for entry in raw :
layerName , directoryName = entry
contents [ layerName ] = directoryName
self . layerContents = contents
2011-09-30 20:41:24 +00:00
def writeLayerContents ( self , layerOrder = None ) :
2011-09-30 20:51:34 +00:00
"""
Write the layercontents . plist file . This method * must * be called
after all glyph sets have been written .
"""
2011-10-06 13:30:11 +00:00
if self . formatVersion < 3 :
return
2011-09-30 20:41:24 +00:00
newOrder = [ ]
for layerName in layerOrder :
if layerName is None :
layerName = DEFAULT_LAYER_NAME
newOrder . append ( layerName )
layerOrder = newOrder
if set ( layerOrder ) != set ( self . layerContents . keys ( ) ) :
raise UFOLibError ( " The layer order contents does not match the glyph sets that have been created. " )
2011-09-19 01:40:21 +00:00
self . _makeDirectory ( )
path = os . path . join ( self . _path , LAYERCONTENTS_FILENAME )
2011-09-30 20:41:24 +00:00
layerContents = [ ( layerName , self . layerContents [ layerName ] ) for layerName in layerOrder ]
2011-09-19 01:40:21 +00:00
self . _writePlist ( layerContents , path )
2008-01-07 17:40:34 +00:00
2011-09-26 19:57:10 +00:00
def _findDirectoryForLayerName ( self , layerName ) :
foundDirectory = None
for existingLayerName , directoryName in self . layerContents . items ( ) :
if layerName is None and directoryName == DEFAULT_GLYPHS_DIRNAME :
foundDirectory = directoryName
break
elif existingLayerName == layerName :
foundDirectory = directoryName
break
if not foundDirectory :
raise UFOLibError ( " Could not locate a glyph set directory for the layer named %s . " % layerName )
return foundDirectory
2011-10-05 19:53:22 +00:00
def getGlyphSet ( self , layerName = None , defaultLayer = True , glyphNameToFileNameFunc = None ) :
2009-02-28 15:47:24 +00:00
"""
2011-09-26 19:57:10 +00:00
Return the GlyphSet object associated with the
2011-09-19 01:40:21 +00:00
appropriate glyph directory in the . ufo .
If layerName is None , the default glyph set
2011-10-05 20:48:36 +00:00
will be used . The defaultLayer flag indictes
that the layer should be saved into the default
glyphs directory .
2011-09-19 01:40:21 +00:00
"""
2011-10-06 13:30:11 +00:00
# only default can be written in < 3
2011-10-06 19:53:38 +00:00
if self . _formatVersion < 3 and ( not defaultLayer or layerName is not None ) :
2011-10-05 19:53:22 +00:00
raise UFOLibError ( " Only the default layer can be writen in UFO %d . " % self . formatVersion )
2011-10-06 13:30:11 +00:00
# locate a layer name when None has been given
2011-10-05 19:53:22 +00:00
if layerName is None and defaultLayer :
2011-10-06 13:30:11 +00:00
for existingLayerName , directory in self . layerContents . items ( ) :
if directory == DEFAULT_GLYPHS_DIRNAME :
layerName = existingLayerName
if layerName is None :
layerName = DEFAULT_LAYER_NAME
elif layerName is None and not defaultLayer :
raise UFOLibError ( " A layer name must be provided for non-default layers. " )
# move along to format specific writing
if self . formatVersion == 1 :
return self . _getGlyphSetFormatVersion1 ( glyphNameToFileNameFunc = glyphNameToFileNameFunc )
elif self . formatVersion == 2 :
return self . _getGlyphSetFormatVersion2 ( glyphNameToFileNameFunc = glyphNameToFileNameFunc )
elif self . formatVersion == 3 :
return self . _getGlyphSetFormatVersion3 ( layerName = layerName , defaultLayer = defaultLayer , glyphNameToFileNameFunc = glyphNameToFileNameFunc )
def _getGlyphSetFormatVersion1 ( self , glyphNameToFileNameFunc = None ) :
2011-10-06 19:53:38 +00:00
glyphDir = self . _makeDirectory ( DEFAULT_GLYPHS_DIRNAME )
2011-10-07 02:13:17 +00:00
return GlyphSet ( glyphDir , glyphNameToFileNameFunc , ufoFormatVersion = 1 )
2011-10-06 13:30:11 +00:00
2011-10-06 19:53:38 +00:00
def _getGlyphSetFormatVersion2 ( self , glyphNameToFileNameFunc = None ) :
glyphDir = self . _makeDirectory ( DEFAULT_GLYPHS_DIRNAME )
2011-10-07 02:13:17 +00:00
return GlyphSet ( glyphDir , glyphNameToFileNameFunc , ufoFormatVersion = 2 )
2011-10-06 13:30:11 +00:00
def _getGlyphSetFormatVersion3 ( self , layerName = None , defaultLayer = True , glyphNameToFileNameFunc = None ) :
2011-10-05 19:53:22 +00:00
# if the default flag is on, make sure that the default in the file
# matches the default being written. also make sure that this layer
# name is not already linked to a non-default layer.
if defaultLayer :
2011-09-19 01:40:21 +00:00
for existingLayerName , directory in self . layerContents . items ( ) :
if directory == DEFAULT_GLYPHS_DIRNAME :
2011-10-05 19:53:22 +00:00
if existingLayerName != layerName :
raise UFOLibError ( " Another layer is already mapped to the default directory. " )
elif existingLayerName == layerName :
raise UFOLibError ( " The layer name is already mapped to a non-default layer. " )
# get an existing directory name
if layerName in self . layerContents :
directory = self . layerContents [ layerName ]
2011-10-06 13:30:11 +00:00
# get a new directory name
2011-09-19 01:40:21 +00:00
else :
2011-10-05 19:53:22 +00:00
if defaultLayer :
2011-09-19 01:40:21 +00:00
directory = DEFAULT_GLYPHS_DIRNAME
else :
# not caching this could be slightly expensive,
# but caching it will be cumbersome
existing = [ d . lower ( ) for d in self . layerContents . values ( ) ]
if not isinstance ( layerName , unicode ) :
try :
layerName = unicode ( layerName )
except UnicodeDecodeError :
raise UFOLibError ( " The specified layer name is not a Unicode string. " )
2011-09-27 15:28:42 +00:00
directory = userNameToFileName ( layerName , existing = existing , prefix = " glyphs. " )
2011-09-19 01:40:21 +00:00
# make the directory
path = os . path . join ( self . _path , directory )
if not os . path . exists ( path ) :
self . _makeDirectory ( subDirectory = directory )
2011-09-30 20:41:24 +00:00
# store the mapping
2011-09-19 01:40:21 +00:00
self . layerContents [ layerName ] = directory
# load the glyph set
2011-10-07 02:13:17 +00:00
return GlyphSet ( path , glyphNameToFileNameFunc = glyphNameToFileNameFunc , ufoFormatVersion = 3 )
2011-09-19 01:40:21 +00:00
2011-10-06 16:09:40 +00:00
def renameGlyphSet ( self , layerName , newLayerName , defaultLayer = False ) :
2011-09-26 19:57:10 +00:00
"""
Rename a glyph set .
Note : if a GlyphSet object has already been retrieved for
layerName , it is up to the caller to inform that object that
the directory it represents has changed .
"""
2011-10-06 13:30:11 +00:00
if self . _formatVersion < 3 :
2011-09-26 20:12:19 +00:00
raise UFOLibError ( " Renaming a glyph set is not allowed in UFO %d . " % self . _formatVersion )
2011-10-06 17:50:53 +00:00
# the new and old names can be the same
# as long as the default is being switched
if layerName == newLayerName :
# if the default is off and the layer is already not the default, skip
if self . layerContents [ layerName ] != DEFAULT_GLYPHS_DIRNAME and not defaultLayer :
return
# if the default is on and the layer is already the default, skip
if self . layerContents [ layerName ] == DEFAULT_GLYPHS_DIRNAME and defaultLayer :
return
else :
# make sure the new layer name doesn't already exist
if newLayerName is None :
newLayerName = DEFAULT_LAYER_NAME
if newLayerName in self . layerContents :
raise UFOLibError ( " A layer named %s already exists. " % newLayerName )
# make sure the default layer doesn't already exist
if defaultLayer and DEFAULT_GLYPHS_DIRNAME in self . layerContents . values ( ) :
raise UFOLibError ( " A default layer already exists. " )
2011-09-26 19:57:10 +00:00
# get the paths
2011-09-27 15:28:42 +00:00
oldDirectory = self . _findDirectoryForLayerName ( layerName )
2011-10-06 16:09:40 +00:00
if defaultLayer :
newDirectory = DEFAULT_GLYPHS_DIRNAME
else :
existing = [ name . lower ( ) for name in self . layerContents . values ( ) ]
newDirectory = userNameToFileName ( newLayerName , existing = existing , prefix = " glyphs. " )
2011-09-26 19:57:10 +00:00
# update the internal mapping
del self . layerContents [ layerName ]
2011-09-27 15:28:42 +00:00
self . layerContents [ newLayerName ] = newDirectory
2011-09-26 19:57:10 +00:00
# do the file system copy
oldDirectory = os . path . join ( self . _path , oldDirectory )
newDirectory = os . path . join ( self . _path , newDirectory )
2011-09-27 15:28:42 +00:00
shutil . move ( oldDirectory , newDirectory )
2011-09-26 19:57:10 +00:00
2011-09-19 01:40:21 +00:00
def deleteGlyphSet ( self , layerName ) :
"""
Remove the glyph set matching layerName .
"""
2011-10-06 13:30:11 +00:00
if self . _formatVersion < 3 :
2011-09-26 20:12:19 +00:00
raise UFOLibError ( " Deleting a glyph set is not allowed in UFO %d . " % self . _formatVersion )
2011-09-27 15:28:42 +00:00
foundDirectory = self . _findDirectoryForLayerName ( layerName )
2011-09-19 01:40:21 +00:00
self . _removeFileForPath ( foundDirectory )
del self . layerContents [ layerName ]
2009-02-28 15:47:24 +00:00
2011-10-11 16:19:11 +00:00
# /images
def writeImage ( self , fileName , data ) :
"""
Write data to fileName in the images directory .
The data must be a valid PNG .
"""
2011-10-11 16:21:58 +00:00
if self . _formatVersion < 3 :
raise UFOLibError ( " Images are not allowed in UFO %d . " % self . _formatVersion )
2011-10-11 16:43:35 +00:00
valid , error = pngValidator ( data = data )
2011-10-11 16:19:11 +00:00
if not valid :
raise UFOLibError ( error )
path = os . path . join ( IMAGES_DIRNAME , fileName )
self . writeBytesToPath ( path , data )
def removeImage ( self , fileName ) :
"""
Remove the file named fileName from the
images directory .
"""
2011-10-11 16:21:58 +00:00
if self . _formatVersion < 3 :
raise UFOLibError ( " Images are not allowed in UFO %d . " % self . _formatVersion )
2011-10-11 16:19:11 +00:00
path = os . path . join ( IMAGES_DIRNAME , fileName )
self . removeFileForPath ( path )
2011-10-19 14:58:35 +00:00
def copyImageFromReader ( self , reader , sourceFileName , destFileName ) :
"""
Copy the sourceFileName in the provided UFOReader to destFileName
in this writer . This uses the most memory efficient method possible
for copying the data possible .
"""
if self . _formatVersion < 3 :
raise UFOLibError ( " Images are not allowed in UFO %d . " % self . _formatVersion )
sourcePath = os . path . join ( " images " , sourceFileName )
destPath = os . path . join ( " images " , destFileName )
self . copyFromReader ( reader , sourcePath , destPath )
2009-02-28 15:47:24 +00:00
# ----------------
# Helper Functions
# ----------------
def makeUFOPath ( path ) :
"""
Return a . ufo pathname .
>> > makeUFOPath ( " /directory/something.ext " )
' /directory/something.ufo '
>> > makeUFOPath ( " /directory/something.another.thing.ext " )
' /directory/something.another.thing.ufo '
"""
dir , name = os . path . split ( path )
name = " . " . join ( [ " . " . join ( name . split ( " . " ) [ : - 1 ] ) , " ufo " ] )
return os . path . join ( dir , name )
def writePlistAtomically ( obj , path ) :
"""
Write a plist for " obj " to " path " . Do this sort of atomically ,
making it harder to cause corrupt files , for example when writePlist
encounters an error halfway during write . This also checks to see
if text matches the text that is already in the file at path .
If so , the file is not rewritten so that the modification date
is preserved .
"""
f = StringIO ( )
writePlist ( obj , f )
data = f . getvalue ( )
writeFileAtomically ( data , path )
2011-09-12 17:49:34 +00:00
def writeFileAtomically ( text , path , encoding = None ) :
"""
Write text into a file at path . Do this sort of atomically
2009-02-28 15:47:24 +00:00
making it harder to cause corrupt files . This also checks to see
if text matches the text that is already in the file at path .
If so , the file is not rewritten so that the modification date
2011-09-12 17:49:34 +00:00
is preserved . An encoding may be passed if needed .
"""
2009-02-28 15:47:24 +00:00
if os . path . exists ( path ) :
2011-09-12 17:49:34 +00:00
f = codecs . open ( path , READ_MODE , encoding = encoding )
2009-02-28 15:47:24 +00:00
oldText = f . read ( )
f . close ( )
if text == oldText :
return
# if the text is empty, remove the existing file
if not text :
os . remove ( path )
if text :
2011-09-12 17:49:34 +00:00
f = codecs . open ( path , WRITE_MODE , encoding = encoding )
2009-02-28 15:47:24 +00:00
f . write ( text )
f . close ( )
2011-09-19 01:40:21 +00:00
# ---------------------------
# Format Conversion Functions
# ---------------------------
def convertUFOFormatVersion1ToFormatVersion2 ( inPath , outPath = None ) :
"""
Function for converting a version format 1 UFO
to version format 2. inPath should be a path
to a UFO . outPath is the path where the new UFO
should be written . If outPath is not given , the
inPath will be used and , therefore , the UFO will
be converted in place . Otherwise , if outPath is
specified , nothing must exist at that path .
"""
if outPath is None :
outPath = inPath
if inPath != outPath and os . path . exists ( outPath ) :
raise UFOLibError ( " A file already exists at %s . " % outPath )
# use a reader for loading most of the data
reader = UFOReader ( inPath )
if reader . formatVersion == 2 :
raise UFOLibError ( " The UFO at %s is already format version 2. " % inPath )
groups = reader . readGroups ( )
kerning = reader . readKerning ( )
libData = reader . readLib ( )
# read the info data manually and convert
infoPath = os . path . join ( inPath , FONTINFO_FILENAME )
if not os . path . exists ( infoPath ) :
infoData = { }
else :
infoData = readPlist ( infoPath )
infoData = _convertFontInfoDataVersion1ToVersion2 ( infoData )
# if the paths are the same, only need to change the
# fontinfo and meta info files.
infoPath = os . path . join ( outPath , FONTINFO_FILENAME )
if inPath == outPath :
metaInfoPath = os . path . join ( inPath , METAINFO_FILENAME )
metaInfo = dict (
creator = " org.robofab.ufoLib " ,
formatVersion = 2
)
writePlistAtomically ( metaInfo , metaInfoPath )
writePlistAtomically ( infoData , infoPath )
# otherwise write everything.
else :
writer = UFOWriter ( outPath , formatVersion = 2 )
writer . writeGroups ( groups )
writer . writeKerning ( kerning )
writer . writeLib ( libData )
# write the info manually
writePlistAtomically ( infoData , infoPath )
# copy the glyph tree
inGlyphs = os . path . join ( inPath , DEFAULT_GLYPHS_DIRNAME )
outGlyphs = os . path . join ( outPath , DEFAULT_GLYPHS_DIRNAME )
if os . path . exists ( inGlyphs ) :
shutil . copytree ( inGlyphs , outGlyphs )
2009-02-28 15:47:24 +00:00
# ----------------------
# fontinfo.plist Support
# ----------------------
2011-09-12 20:56:12 +00:00
# Version Validators
2009-02-28 15:47:24 +00:00
2011-09-12 20:56:12 +00:00
# There is no version 1 validator and there shouldn't be.
# The version 1 spec was very loose and there were numerous
# cases of invalid values.
2009-02-28 15:47:24 +00:00
def validateFontInfoVersion2ValueForAttribute ( attr , value ) :
"""
This performs very basic validation of the value for attribute
2011-09-14 21:13:27 +00:00
following the UFO 2 fontinfo . plist specification . The results
2009-02-28 15:47:24 +00:00
of this should not be interpretted as * correct * for the font
that they are part of . This merely indicates that the value
is of the proper type and , where the specification defines
a set range of possible values for an attribute , that the
value is in the accepted range .
"""
2011-09-18 12:24:29 +00:00
dataValidationDict = fontInfoAttributesVersion2ValueData [ attr ]
2009-02-28 15:47:24 +00:00
valueType = dataValidationDict . get ( " type " )
validator = dataValidationDict . get ( " valueValidator " )
valueOptions = dataValidationDict . get ( " valueOptions " )
# have specific options for the validator
if valueOptions is not None :
isValidValue = validator ( value , valueOptions )
# no specific options
else :
2011-09-28 13:42:09 +00:00
if validator == genericTypeValidator :
2009-02-28 15:47:24 +00:00
isValidValue = validator ( value , valueType )
else :
isValidValue = validator ( value )
return isValidValue
2011-09-18 12:24:29 +00:00
def validateInfoVersion2Data ( infoData ) :
"""
This performs very basic validation of the value for infoData
following the UFO 2 fontinfo . plist specification . The results
of this should not be interpretted as * correct * for the font
that they are part of . This merely indicates that the values
are of the proper type and , where the specification defines
a set range of possible values for an attribute , that the
value is in the accepted range .
"""
2009-02-28 15:47:24 +00:00
validInfoData = { }
for attr , value in infoData . items ( ) :
isValidValue = validateFontInfoVersion2ValueForAttribute ( attr , value )
if not isValidValue :
raise UFOLibError ( " Invalid value for attribute %s ( %s ). " % ( attr , repr ( value ) ) )
else :
validInfoData [ attr ] = value
2011-09-28 13:59:47 +00:00
return validInfoData
2009-02-28 15:47:24 +00:00
2011-09-14 21:13:27 +00:00
def validateFontInfoVersion3ValueForAttribute ( attr , value ) :
"""
This performs very basic validation of the value for attribute
2011-09-18 12:24:29 +00:00
following the UFO 3 fontinfo . plist specification . The results
2011-09-14 21:13:27 +00:00
of this should not be interpretted as * correct * for the font
that they are part of . This merely indicates that the value
is of the proper type and , where the specification defines
a set range of possible values for an attribute , that the
value is in the accepted range .
"""
2011-09-18 12:24:29 +00:00
dataValidationDict = fontInfoAttributesVersion3ValueData [ attr ]
2011-09-14 21:13:27 +00:00
valueType = dataValidationDict . get ( " type " )
validator = dataValidationDict . get ( " valueValidator " )
valueOptions = dataValidationDict . get ( " valueOptions " )
# have specific options for the validator
if valueOptions is not None :
isValidValue = validator ( value , valueOptions )
# no specific options
else :
2011-09-28 13:42:09 +00:00
if validator == genericTypeValidator :
2011-09-14 21:13:27 +00:00
isValidValue = validator ( value , valueType )
else :
isValidValue = validator ( value )
return isValidValue
2011-09-18 12:24:29 +00:00
def validateInfoVersion3Data ( infoData ) :
"""
This performs very basic validation of the value for infoData
following the UFO 3 fontinfo . plist specification . The results
of this should not be interpretted as * correct * for the font
that they are part of . This merely indicates that the values
are of the proper type and , where the specification defines
a set range of possible values for an attribute , that the
value is in the accepted range .
"""
2011-09-14 21:13:27 +00:00
validInfoData = { }
for attr , value in infoData . items ( ) :
isValidValue = validateFontInfoVersion3ValueForAttribute ( attr , value )
if not isValidValue :
raise UFOLibError ( " Invalid value for attribute %s ( %s ). " % ( attr , repr ( value ) ) )
else :
validInfoData [ attr ] = value
2011-09-28 13:59:47 +00:00
return validInfoData
2011-09-14 21:13:27 +00:00
2011-09-12 20:56:12 +00:00
# Value Options
2011-10-13 00:43:23 +00:00
fontInfoOpenTypeHeadFlagsOptions = range ( 0 , 15 )
2011-10-13 00:41:58 +00:00
fontInfoOpenTypeOS2SelectionOptions = [ 1 , 2 , 3 , 4 , 7 , 8 , 9 ]
2011-09-18 12:24:29 +00:00
fontInfoOpenTypeOS2UnicodeRangesOptions = range ( 0 , 128 )
fontInfoOpenTypeOS2CodePageRangesOptions = range ( 0 , 64 )
fontInfoOpenTypeOS2TypeOptions = [ 0 , 1 , 2 , 3 , 8 , 9 ]
2011-09-12 20:56:12 +00:00
# Version Attribute Definitions
2009-02-28 15:47:24 +00:00
# This defines the attributes, types and, in some
# cases the possible values, that can exist is
# fontinfo.plist.
2011-09-12 20:56:12 +00:00
fontInfoAttributesVersion1 = set ( [
" familyName " ,
" styleName " ,
" fullName " ,
" fontName " ,
" menuName " ,
" fontStyle " ,
" note " ,
" versionMajor " ,
" versionMinor " ,
" year " ,
" copyright " ,
" notice " ,
" trademark " ,
" license " ,
" licenseURL " ,
" createdBy " ,
" designer " ,
" designerURL " ,
" vendorURL " ,
" unitsPerEm " ,
" ascender " ,
" descender " ,
" capHeight " ,
" xHeight " ,
" defaultWidth " ,
" slantAngle " ,
" italicAngle " ,
" widthName " ,
" weightName " ,
" weightValue " ,
" fondName " ,
" otFamilyName " ,
" otStyleName " ,
" otMacName " ,
" msCharSet " ,
" fondID " ,
" uniqueID " ,
" ttVendor " ,
" ttUniqueID " ,
" ttVersion " ,
] )
2009-02-28 15:47:24 +00:00
2011-09-18 12:24:29 +00:00
fontInfoAttributesVersion2ValueData = {
2011-09-16 11:23:44 +00:00
" familyName " : dict ( type = basestring ) ,
" styleName " : dict ( type = basestring ) ,
" styleMapFamilyName " : dict ( type = basestring ) ,
2011-09-18 12:24:29 +00:00
" styleMapStyleName " : dict ( type = basestring , valueValidator = fontInfoStyleMapStyleNameValidator ) ,
2009-02-28 15:47:24 +00:00
" versionMajor " : dict ( type = int ) ,
" versionMinor " : dict ( type = int ) ,
" year " : dict ( type = int ) ,
2011-09-16 11:23:44 +00:00
" copyright " : dict ( type = basestring ) ,
" trademark " : dict ( type = basestring ) ,
2009-02-28 15:47:24 +00:00
" unitsPerEm " : dict ( type = ( int , float ) ) ,
" descender " : dict ( type = ( int , float ) ) ,
" xHeight " : dict ( type = ( int , float ) ) ,
" capHeight " : dict ( type = ( int , float ) ) ,
" ascender " : dict ( type = ( int , float ) ) ,
" italicAngle " : dict ( type = ( float , int ) ) ,
2011-09-16 11:23:44 +00:00
" note " : dict ( type = basestring ) ,
2011-09-18 12:24:29 +00:00
" openTypeHeadCreated " : dict ( type = basestring , valueValidator = fontInfoOpenTypeHeadCreatedValidator ) ,
2009-02-28 15:47:24 +00:00
" openTypeHeadLowestRecPPEM " : dict ( type = ( int , float ) ) ,
2011-09-28 13:42:09 +00:00
" openTypeHeadFlags " : dict ( type = " integerList " , valueValidator = genericIntListValidator , valueOptions = fontInfoOpenTypeHeadFlagsOptions ) ,
2009-02-28 15:47:24 +00:00
" openTypeHheaAscender " : dict ( type = ( int , float ) ) ,
" openTypeHheaDescender " : dict ( type = ( int , float ) ) ,
" openTypeHheaLineGap " : dict ( type = ( int , float ) ) ,
" openTypeHheaCaretSlopeRise " : dict ( type = int ) ,
" openTypeHheaCaretSlopeRun " : dict ( type = int ) ,
" openTypeHheaCaretOffset " : dict ( type = ( int , float ) ) ,
2011-09-16 11:23:44 +00:00
" openTypeNameDesigner " : dict ( type = basestring ) ,
" openTypeNameDesignerURL " : dict ( type = basestring ) ,
" openTypeNameManufacturer " : dict ( type = basestring ) ,
" openTypeNameManufacturerURL " : dict ( type = basestring ) ,
" openTypeNameLicense " : dict ( type = basestring ) ,
" openTypeNameLicenseURL " : dict ( type = basestring ) ,
" openTypeNameVersion " : dict ( type = basestring ) ,
" openTypeNameUniqueID " : dict ( type = basestring ) ,
" openTypeNameDescription " : dict ( type = basestring ) ,
" openTypeNamePreferredFamilyName " : dict ( type = basestring ) ,
" openTypeNamePreferredSubfamilyName " : dict ( type = basestring ) ,
" openTypeNameCompatibleFullName " : dict ( type = basestring ) ,
" openTypeNameSampleText " : dict ( type = basestring ) ,
" openTypeNameWWSFamilyName " : dict ( type = basestring ) ,
" openTypeNameWWSSubfamilyName " : dict ( type = basestring ) ,
2011-09-18 12:24:29 +00:00
" openTypeOS2WidthClass " : dict ( type = int , valueValidator = fontInfoOpenTypeOS2WidthClassValidator ) ,
" openTypeOS2WeightClass " : dict ( type = int , valueValidator = fontInfoOpenTypeOS2WeightClassValidator ) ,
2011-09-28 13:42:09 +00:00
" openTypeOS2Selection " : dict ( type = " integerList " , valueValidator = genericIntListValidator , valueOptions = fontInfoOpenTypeOS2SelectionOptions ) ,
2011-09-16 11:23:44 +00:00
" openTypeOS2VendorID " : dict ( type = basestring ) ,
2011-09-18 12:24:29 +00:00
" openTypeOS2Panose " : dict ( type = " integerList " , valueValidator = fontInfoVersion2OpenTypeOS2PanoseValidator ) ,
" openTypeOS2FamilyClass " : dict ( type = " integerList " , valueValidator = fontInfoOpenTypeOS2FamilyClassValidator ) ,
2011-09-28 13:42:09 +00:00
" openTypeOS2UnicodeRanges " : dict ( type = " integerList " , valueValidator = genericIntListValidator , valueOptions = fontInfoOpenTypeOS2UnicodeRangesOptions ) ,
" openTypeOS2CodePageRanges " : dict ( type = " integerList " , valueValidator = genericIntListValidator , valueOptions = fontInfoOpenTypeOS2CodePageRangesOptions ) ,
2009-02-28 15:47:24 +00:00
" openTypeOS2TypoAscender " : dict ( type = ( int , float ) ) ,
" openTypeOS2TypoDescender " : dict ( type = ( int , float ) ) ,
" openTypeOS2TypoLineGap " : dict ( type = ( int , float ) ) ,
" openTypeOS2WinAscent " : dict ( type = ( int , float ) ) ,
" openTypeOS2WinDescent " : dict ( type = ( int , float ) ) ,
2011-09-28 13:42:09 +00:00
" openTypeOS2Type " : dict ( type = " integerList " , valueValidator = genericIntListValidator , valueOptions = fontInfoOpenTypeOS2TypeOptions ) ,
2009-02-28 15:47:24 +00:00
" openTypeOS2SubscriptXSize " : dict ( type = ( int , float ) ) ,
" openTypeOS2SubscriptYSize " : dict ( type = ( int , float ) ) ,
" openTypeOS2SubscriptXOffset " : dict ( type = ( int , float ) ) ,
" openTypeOS2SubscriptYOffset " : dict ( type = ( int , float ) ) ,
" openTypeOS2SuperscriptXSize " : dict ( type = ( int , float ) ) ,
" openTypeOS2SuperscriptYSize " : dict ( type = ( int , float ) ) ,
" openTypeOS2SuperscriptXOffset " : dict ( type = ( int , float ) ) ,
" openTypeOS2SuperscriptYOffset " : dict ( type = ( int , float ) ) ,
" openTypeOS2StrikeoutSize " : dict ( type = ( int , float ) ) ,
" openTypeOS2StrikeoutPosition " : dict ( type = ( int , float ) ) ,
" openTypeVheaVertTypoAscender " : dict ( type = ( int , float ) ) ,
" openTypeVheaVertTypoDescender " : dict ( type = ( int , float ) ) ,
" openTypeVheaVertTypoLineGap " : dict ( type = ( int , float ) ) ,
" openTypeVheaCaretSlopeRise " : dict ( type = int ) ,
" openTypeVheaCaretSlopeRun " : dict ( type = int ) ,
" openTypeVheaCaretOffset " : dict ( type = ( int , float ) ) ,
2011-09-16 11:23:44 +00:00
" postscriptFontName " : dict ( type = basestring ) ,
" postscriptFullName " : dict ( type = basestring ) ,
2009-02-28 15:47:24 +00:00
" postscriptSlantAngle " : dict ( type = ( float , int ) ) ,
" postscriptUniqueID " : dict ( type = int ) ,
" postscriptUnderlineThickness " : dict ( type = ( int , float ) ) ,
" postscriptUnderlinePosition " : dict ( type = ( int , float ) ) ,
" postscriptIsFixedPitch " : dict ( type = bool ) ,
2011-09-18 12:24:29 +00:00
" postscriptBlueValues " : dict ( type = " integerList " , valueValidator = fontInfoPostscriptBluesValidator ) ,
" postscriptOtherBlues " : dict ( type = " integerList " , valueValidator = fontInfoPostscriptOtherBluesValidator ) ,
" postscriptFamilyBlues " : dict ( type = " integerList " , valueValidator = fontInfoPostscriptBluesValidator ) ,
" postscriptFamilyOtherBlues " : dict ( type = " integerList " , valueValidator = fontInfoPostscriptOtherBluesValidator ) ,
" postscriptStemSnapH " : dict ( type = " integerList " , valueValidator = fontInfoPostscriptStemsValidator ) ,
" postscriptStemSnapV " : dict ( type = " integerList " , valueValidator = fontInfoPostscriptStemsValidator ) ,
2009-02-28 15:47:24 +00:00
" postscriptBlueFuzz " : dict ( type = ( int , float ) ) ,
" postscriptBlueShift " : dict ( type = ( int , float ) ) ,
" postscriptBlueScale " : dict ( type = ( float , int ) ) ,
" postscriptForceBold " : dict ( type = bool ) ,
" postscriptDefaultWidthX " : dict ( type = ( int , float ) ) ,
" postscriptNominalWidthX " : dict ( type = ( int , float ) ) ,
2011-09-16 11:23:44 +00:00
" postscriptWeightName " : dict ( type = basestring ) ,
" postscriptDefaultCharacter " : dict ( type = basestring ) ,
2011-09-18 12:24:29 +00:00
" postscriptWindowsCharacterSet " : dict ( type = int , valueValidator = fontInfoPostscriptWindowsCharacterSetValidator ) ,
2009-02-28 15:47:24 +00:00
" macintoshFONDFamilyID " : dict ( type = int ) ,
2011-09-16 11:23:44 +00:00
" macintoshFONDName " : dict ( type = basestring ) ,
2009-02-28 15:47:24 +00:00
}
2011-09-18 12:24:29 +00:00
fontInfoAttributesVersion2 = set ( fontInfoAttributesVersion2ValueData . keys ( ) )
fontInfoAttributesVersion3ValueData = deepcopy ( fontInfoAttributesVersion2ValueData )
fontInfoAttributesVersion3ValueData . update ( {
2011-09-28 13:42:09 +00:00
" versionMinor " : dict ( type = int , valueValidator = genericNonNegativeIntValidator ) ,
" unitsPerEm " : dict ( type = ( int , float ) , valueValidator = genericNonNegativeNumberValidator ) ,
" openTypeHeadLowestRecPPEM " : dict ( type = ( int , float ) , valueValidator = genericNonNegativeNumberValidator ) ,
2011-09-18 12:24:29 +00:00
" openTypeOS2Panose " : dict ( type = " integerList " , valueValidator = fontInfoVersion3OpenTypeOS2PanoseValidator ) ,
2011-09-28 13:42:09 +00:00
" openTypeOS2WinAscent " : dict ( type = ( int , float ) , valueValidator = genericNonNegativeNumberValidator ) ,
" openTypeOS2WinDescent " : dict ( type = ( int , float ) , valueValidator = genericNonNegativeNumberValidator ) ,
2011-09-18 12:24:29 +00:00
" openTypeGaspRangeRecords " : dict ( type = " dictList " , valueValidator = fontInfoOpenTypeGaspRangeRecordsValidator ) ,
" openTypeNameRecords " : dict ( type = " dictList " , valueValidator = fontInfoOpenTypeNameRecordsValidator ) ,
2011-09-28 13:42:09 +00:00
" woffMajorVersion " : dict ( type = int , valueValidator = genericNonNegativeIntValidator ) ,
" woffMinorVersion " : dict ( type = int , valueValidator = genericNonNegativeIntValidator ) ,
2011-09-18 12:24:29 +00:00
" woffMetadataUniqueID " : dict ( type = dict , valueValidator = fontInfoWOFFMetadataUniqueIDValidator ) ,
" woffMetadataVendor " : dict ( type = dict , valueValidator = fontInfoWOFFMetadataVendorValidator ) ,
" woffMetadataCredits " : dict ( type = dict , valueValidator = fontInfoWOFFMetadataCreditsValidator ) ,
" woffMetadataDescription " : dict ( type = dict , valueValidator = fontInfoWOFFMetadataDescriptionValidator ) ,
" woffMetadataLicense " : dict ( type = dict , valueValidator = fontInfoWOFFMetadataLicenseValidator ) ,
" woffMetadataCopyright " : dict ( type = dict , valueValidator = fontInfoWOFFMetadataCopyrightValidator ) ,
" woffMetadataTrademark " : dict ( type = dict , valueValidator = fontInfoWOFFMetadataTrademarkValidator ) ,
" woffMetadataLicensee " : dict ( type = dict , valueValidator = fontInfoWOFFMetadataLicenseeValidator ) ,
" woffMetadataExtensions " : dict ( type = list , valueValidator = fontInfoWOFFMetadataExtensionsValidator ) ,
2011-09-28 13:51:52 +00:00
" guidelines " : dict ( type = list , valueValidator = guidelinesValidator )
2011-09-12 22:15:15 +00:00
} )
2011-10-04 01:07:22 +00:00
fontInfoAttributesVersion3 = set ( fontInfoAttributesVersion3ValueData . keys ( ) )
2011-09-12 22:15:15 +00:00
2009-02-28 15:47:24 +00:00
# insert the type validator for all attrs that
# have no defined validator.
2011-09-18 12:24:29 +00:00
for attr , dataDict in fontInfoAttributesVersion2ValueData . items ( ) :
2009-02-28 15:47:24 +00:00
if " valueValidator " not in dataDict :
2011-09-28 13:42:09 +00:00
dataDict [ " valueValidator " ] = genericTypeValidator
2009-02-28 15:47:24 +00:00
2011-09-18 12:24:29 +00:00
for attr , dataDict in fontInfoAttributesVersion3ValueData . items ( ) :
2011-09-14 21:13:27 +00:00
if " valueValidator " not in dataDict :
2011-09-28 13:42:09 +00:00
dataDict [ " valueValidator " ] = genericTypeValidator
2011-09-14 21:13:27 +00:00
2009-02-28 15:47:24 +00:00
# Version Conversion Support
# These are used from converting from version 1
# to version 2 or vice-versa.
def _flipDict ( d ) :
flipped = { }
for key , value in d . items ( ) :
flipped [ value ] = key
return flipped
2011-09-18 12:24:29 +00:00
fontInfoAttributesVersion1To2 = {
2009-02-28 15:47:24 +00:00
" menuName " : " styleMapFamilyName " ,
" designer " : " openTypeNameDesigner " ,
" designerURL " : " openTypeNameDesignerURL " ,
" createdBy " : " openTypeNameManufacturer " ,
" vendorURL " : " openTypeNameManufacturerURL " ,
" license " : " openTypeNameLicense " ,
" licenseURL " : " openTypeNameLicenseURL " ,
" ttVersion " : " openTypeNameVersion " ,
" ttUniqueID " : " openTypeNameUniqueID " ,
" notice " : " openTypeNameDescription " ,
" otFamilyName " : " openTypeNamePreferredFamilyName " ,
" otStyleName " : " openTypeNamePreferredSubfamilyName " ,
" otMacName " : " openTypeNameCompatibleFullName " ,
" weightName " : " postscriptWeightName " ,
" weightValue " : " openTypeOS2WeightClass " ,
" ttVendor " : " openTypeOS2VendorID " ,
" uniqueID " : " postscriptUniqueID " ,
" fontName " : " postscriptFontName " ,
" fondID " : " macintoshFONDFamilyID " ,
" fondName " : " macintoshFONDName " ,
" defaultWidth " : " postscriptDefaultWidthX " ,
" slantAngle " : " postscriptSlantAngle " ,
" fullName " : " postscriptFullName " ,
# require special value conversion
" fontStyle " : " styleMapStyleName " ,
" widthName " : " openTypeOS2WidthClass " ,
" msCharSet " : " postscriptWindowsCharacterSet "
}
2011-09-18 12:24:29 +00:00
fontInfoAttributesVersion2To1 = _flipDict ( fontInfoAttributesVersion1To2 )
deprecatedFontInfoAttributesVersion2 = set ( fontInfoAttributesVersion1To2 . keys ( ) )
2009-02-28 15:47:24 +00:00
_fontStyle1To2 = {
64 : " regular " ,
1 : " italic " ,
32 : " bold " ,
33 : " bold italic "
}
_fontStyle2To1 = _flipDict ( _fontStyle1To2 )
# Some UFO 1 files have 0
_fontStyle1To2 [ 0 ] = " regular "
_widthName1To2 = {
" Ultra-condensed " : 1 ,
" Extra-condensed " : 2 ,
" Condensed " : 3 ,
" Semi-condensed " : 4 ,
" Medium (normal) " : 5 ,
" Semi-expanded " : 6 ,
" Expanded " : 7 ,
" Extra-expanded " : 8 ,
" Ultra-expanded " : 9
}
_widthName2To1 = _flipDict ( _widthName1To2 )
# FontLab's default width value is "Normal".
# Many format version 1 UFOs will have this.
_widthName1To2 [ " Normal " ] = 5
# FontLab has an "All" width value. In UFO 1
# move this up to "Normal".
_widthName1To2 [ " All " ] = 5
# "medium" appears in a lot of UFO 1 files.
_widthName1To2 [ " medium " ] = 5
2009-12-03 14:44:41 +00:00
# "Medium" appears in a lot of UFO 1 files.
_widthName1To2 [ " Medium " ] = 5
2009-02-28 15:47:24 +00:00
_msCharSet1To2 = {
0 : 1 ,
1 : 2 ,
2 : 3 ,
77 : 4 ,
128 : 5 ,
129 : 6 ,
130 : 7 ,
134 : 8 ,
136 : 9 ,
161 : 10 ,
162 : 11 ,
163 : 12 ,
177 : 13 ,
178 : 14 ,
186 : 15 ,
200 : 16 ,
204 : 17 ,
222 : 18 ,
238 : 19 ,
255 : 20
}
_msCharSet2To1 = _flipDict ( _msCharSet1To2 )
def convertFontInfoValueForAttributeFromVersion1ToVersion2 ( attr , value ) :
"""
Convert value from version 1 to version 2 format .
Returns the new attribute name and the converted value .
If the value is None , None will be returned for the new value .
"""
# convert floats to ints if possible
if isinstance ( value , float ) :
if int ( value ) == value :
value = int ( value )
if value is not None :
if attr == " fontStyle " :
v = _fontStyle1To2 . get ( value )
if v is None :
raise UFOLibError ( " Cannot convert value ( %s ) for attribute %s . " % ( repr ( value ) , attr ) )
value = v
elif attr == " widthName " :
v = _widthName1To2 . get ( value )
if v is None :
raise UFOLibError ( " Cannot convert value ( %s ) for attribute %s . " % ( repr ( value ) , attr ) )
value = v
elif attr == " msCharSet " :
v = _msCharSet1To2 . get ( value )
if v is None :
raise UFOLibError ( " Cannot convert value ( %s ) for attribute %s . " % ( repr ( value ) , attr ) )
value = v
2011-09-18 12:24:29 +00:00
attr = fontInfoAttributesVersion1To2 . get ( attr , attr )
2009-02-28 15:47:24 +00:00
return attr , value
def convertFontInfoValueForAttributeFromVersion2ToVersion1 ( attr , value ) :
"""
Convert value from version 2 to version 1 format .
Returns the new attribute name and the converted value .
If the value is None , None will be returned for the new value .
"""
if value is not None :
if attr == " styleMapStyleName " :
value = _fontStyle2To1 . get ( value )
elif attr == " openTypeOS2WidthClass " :
value = _widthName2To1 . get ( value )
elif attr == " postscriptWindowsCharacterSet " :
value = _msCharSet2To1 . get ( value )
2011-09-18 12:24:29 +00:00
attr = fontInfoAttributesVersion2To1 . get ( attr , attr )
2009-02-28 15:47:24 +00:00
return attr , value
def _convertFontInfoDataVersion1ToVersion2 ( data ) :
converted = { }
for attr , value in data . items ( ) :
# FontLab gives -1 for the weightValue
# for fonts wil no defined value. Many
# format version 1 UFOs will have this.
if attr == " weightValue " and value == - 1 :
continue
newAttr , newValue = convertFontInfoValueForAttributeFromVersion1ToVersion2 ( attr , value )
# skip if the attribute is not part of version 2
if newAttr not in fontInfoAttributesVersion2 :
continue
# catch values that can't be converted
if value is None :
raise UFOLibError ( " Cannot convert value ( %s ) for attribute %s . " % ( repr ( value ) , newAttr ) )
# store
converted [ newAttr ] = newValue
return converted
def _convertFontInfoDataVersion2ToVersion1 ( data ) :
converted = { }
for attr , value in data . items ( ) :
newAttr , newValue = convertFontInfoValueForAttributeFromVersion2ToVersion1 ( attr , value )
# only take attributes that are registered for version 1
if newAttr not in fontInfoAttributesVersion1 :
continue
# catch values that can't be converted
if value is None :
raise UFOLibError ( " Cannot convert value ( %s ) for attribute %s . " % ( repr ( value ) , newAttr ) )
# store
converted [ newAttr ] = newValue
return converted
2011-09-14 21:13:27 +00:00
def _convertFontInfoDataVersion3ToVersion2 ( data ) :
converted = { }
for attr , value in data . items ( ) :
# only take attributes that are registered for version 2
if attr not in fontInfoAttributesVersion2 :
continue
# store
converted [ attr ] = value
return converted
2009-02-28 15:47:24 +00:00
if __name__ == " __main__ " :
import doctest
doctest . testmod ( )