2018-07-14 19:22:48 +01:00
from __future__ import absolute_import , unicode_literals
2018-10-09 18:20:10 +01:00
import sys
2015-11-02 13:16:56 +00:00
import os
from copy import deepcopy
2018-10-10 10:54:08 +01:00
import logging
2018-10-08 18:01:01 +01:00
import zipfile
import enum
2018-10-23 20:30:52 +01:00
from collections import OrderedDict
2018-10-08 18:01:01 +01:00
import fs
import fs . base
import fs . subfs
import fs . errors
import fs . copy
import fs . osfs
import fs . zipfs
import fs . tempfs
import fs . tools
2018-07-15 21:32:13 +01:00
from fontTools . misc . py23 import basestring , unicode , tounicode
2018-10-17 17:41:43 +01:00
from fontTools . misc import plistlib
from fontTools . ufoLib . validators import *
from fontTools . ufoLib . filenames import userNameToFileName
from fontTools . ufoLib . converters import convertUFO1OrUFO2KerningToUFO3Kerning
from fontTools . ufoLib . errors import UFOLibError
from fontTools . ufoLib . utils import datetimeAsTimestamp , fsdecode , numberTypes
2016-05-02 23:06:25 -04:00
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 .
2011-12-06 19:13:57 +00:00
The UFOReader and UFOWriter classes support versions 1 , 2 and 3
of the specification .
2009-02-28 15:47:24 +00:00
2011-12-06 19:13:57 +00:00
Sets that list the font info attribute names for the fontinfo . plist
formats are available for external use . These are :
2009-02-28 15:47:24 +00:00
fontInfoAttributesVersion1
fontInfoAttributesVersion2
2011-12-06 19:13:57 +00:00
fontInfoAttributesVersion3
2009-02-28 15:47:24 +00:00
A set listing the fontinfo . plist attributes that were deprecated
in version 2 is available for external use :
deprecatedFontInfoAttributesVersion2
2011-12-06 19:13:57 +00:00
Functions that do basic validation on values for fontinfo . plist
are available for external use . These are
validateFontInfoVersion2ValueForAttribute
validateFontInfoVersion3ValueForAttribute
2009-02-28 15:47:24 +00:00
2015-11-08 11:11:11 +01:00
Value conversion functions are available for converting
2009-02-28 15:47:24 +00:00
fontinfo . plist values between the possible format versions .
convertFontInfoValueForAttributeFromVersion1ToVersion2
convertFontInfoValueForAttributeFromVersion2ToVersion1
2011-12-06 20:25:49 +00:00
convertFontInfoValueForAttributeFromVersion2ToVersion3
convertFontInfoValueForAttributeFromVersion3ToVersion2
2008-01-07 17:40:34 +00:00
"""
2009-02-28 15:47:24 +00:00
__all__ = [
2018-04-03 17:36:41 -07:00
" makeUFOPath " ,
2009-02-28 15:47:24 +00:00
" UFOLibError " ,
" UFOReader " ,
" UFOWriter " ,
2018-10-23 20:40:53 +01:00
" UFOReaderWriter " ,
2018-10-10 09:57:14 +01:00
" UFOFileStructure " ,
2009-02-28 15:47:24 +00:00
" fontInfoAttributesVersion1 " ,
" fontInfoAttributesVersion2 " ,
2011-12-06 19:13:57 +00:00
" fontInfoAttributesVersion3 " ,
2009-02-28 15:47:24 +00:00
" deprecatedFontInfoAttributesVersion2 " ,
" validateFontInfoVersion2ValueForAttribute " ,
2011-12-06 19:13:57 +00:00
" validateFontInfoVersion3ValueForAttribute " ,
2009-02-28 15:47:24 +00:00
" convertFontInfoValueForAttributeFromVersion1ToVersion2 " ,
" convertFontInfoValueForAttributeFromVersion2ToVersion1 "
2008-01-07 17:40:34 +00:00
]
2018-10-11 11:31:35 +01:00
__version__ = " 3.0.0 "
2017-05-24 18:37:52 +01:00
2008-01-07 17:40:34 +00:00
2018-10-10 10:54:08 +01:00
logger = logging . getLogger ( __name__ )
2018-10-09 14:13:28 +01:00
# ---------
# Constants
# ---------
2009-02-28 15:47:24 +00:00
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 "
2015-11-02 13:16:56 +00:00
LIB_FILENAME = " lib.plist "
2009-02-28 15:47:24 +00:00
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
2018-10-08 18:01:01 +01:00
class UFOFileStructure ( enum . Enum ) :
ZIP = " zip "
PACKAGE = " package "
2018-10-09 14:13:28 +01:00
# --------------
2018-10-09 15:12:19 +01:00
# Shared Methods
2018-10-09 14:13:28 +01:00
# --------------
2018-10-23 20:41:35 +01:00
class _UFOBaseIO ( object ) :
2018-10-09 14:13:28 +01:00
2018-10-23 19:43:37 +01:00
def _getFileModificationTime ( self , path ) :
"""
Returns the modification time for the file at the given path , as a
floating point number giving the number of seconds since the epoch .
The path must be relative to the UFO path .
Returns None if the file does not exist .
"""
try :
dt = self . fs . getinfo ( fsdecode ( path ) , namespaces = [ " details " ] ) . modified
except ( fs . errors . MissingInfoNamespace , fs . errors . ResourceNotFound ) :
return None
else :
return datetimeAsTimestamp ( dt )
2018-10-09 14:13:28 +01:00
2018-10-23 19:43:37 +01:00
def _getPlist ( self , fileName , default = None ) :
"""
Read a property list relative to the UFO filesystem ' s root.
Raises UFOLibError if the file is missing and default is None ,
otherwise default is returned .
2018-10-09 14:13:28 +01:00
2018-10-23 19:43:37 +01:00
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 .
"""
try :
with self . fs . open ( fileName , " rb " ) as f :
return plistlib . load ( f )
except fs . errors . ResourceNotFound :
if default is None :
raise UFOLibError (
" ' %s ' is missing on %s . This file is required "
% ( fileName , self . fs )
)
else :
return default
except Exception as e :
# TODO(anthrotype): try to narrow this down a little
2018-10-09 14:13:28 +01:00
raise UFOLibError (
2018-10-23 19:43:37 +01:00
" ' %s ' could not be read on %s : %s " % ( fileName , self . fs , e )
2018-10-09 14:13:28 +01:00
)
2018-10-23 19:43:37 +01:00
def _writePlist ( self , fileName , obj ) :
"""
Write a property list to a file relative to the UFO filesystem ' s root.
2018-10-09 14:13:28 +01:00
2018-10-23 19:43:37 +01:00
Do this sort of atomically , making it harder to corrupt existing files ,
for example when plistlib 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 .
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 .
"""
if self . _havePreviousFile :
2018-10-09 14:13:28 +01:00
try :
2018-10-23 19:43:37 +01:00
data = plistlib . dumps ( obj )
2018-10-09 14:13:28 +01:00
except Exception as e :
raise UFOLibError (
" ' %s ' could not be written on %s because "
" the data is not properly formatted: %s "
% ( fileName , self . fs , e )
)
2018-10-23 19:43:37 +01:00
if self . fs . exists ( fileName ) and data == self . fs . getbytes ( fileName ) :
return
self . fs . setbytes ( fileName , data )
else :
with self . fs . openbin ( fileName , mode = " w " ) as fp :
try :
plistlib . dump ( obj , fp )
except Exception as e :
raise UFOLibError (
" ' %s ' could not be written on %s because "
" the data is not properly formatted: %s "
% ( fileName , self . fs , e )
)
2018-10-09 14:13:28 +01:00
2009-02-28 15:47:24 +00:00
# ----------
# UFO Reader
# ----------
2008-01-07 17:40:34 +00:00
2018-10-23 20:41:35 +01:00
class UFOReader ( _UFOBaseIO ) :
2009-02-28 15:47:24 +00:00
2018-06-11 11:07:29 -05:00
"""
Read the various components of the . ufo .
2009-02-28 15:47:24 +00:00
2018-07-04 13:48:37 -05:00
By default read data is validated . Set ` ` validate ` ` to
` ` False ` ` to not validate the data .
2018-06-11 11:07:29 -05:00
"""
2009-02-28 15:47:24 +00:00
2018-07-04 13:48:37 -05:00
def __init__ ( self , path , validate = True ) :
2018-10-08 18:01:01 +01:00
if hasattr ( path , " __fspath__ " ) : # support os.PathLike objects
path = path . __fspath__ ( )
if isinstance ( path , basestring ) :
2018-10-10 09:57:02 +01:00
structure = _sniffFileStructure ( path )
2018-10-08 18:01:01 +01:00
try :
if structure is UFOFileStructure . ZIP :
parentFS = fs . zipfs . ZipFS ( path , write = False , encoding = " utf-8 " )
else :
parentFS = fs . osfs . OSFS ( path )
except fs . errors . CreateFailed as e :
raise UFOLibError ( " unable to open ' %s ' : %s " % ( path , e ) )
if structure is UFOFileStructure . ZIP :
# .ufoz zip files must contain a single root directory, with arbitrary
# name, containing all the UFO files
rootDirs = [
p . name for p in parentFS . scandir ( " / " )
# exclude macOS metadata contained in zip file
if p . is_dir and p . name != " __MACOSX "
]
if len ( rootDirs ) == 1 :
# 'ClosingSubFS' ensures that the parent zip file is closed when
# its root subdirectory is closed
self . fs = parentFS . opendir (
rootDirs [ 0 ] , factory = fs . subfs . ClosingSubFS
)
else :
raise UFOLibError (
" Expected exactly 1 root directory, found %d " % len ( rootDirs )
)
else :
# normal UFO 'packages' are just a single folder
self . fs = parentFS
# when passed a path string, we make sure we close the newly opened fs
# upon calling UFOReader.close method or context manager's __exit__
self . _shouldClose = True
self . _fileStructure = structure
elif isinstance ( path , fs . base . FS ) :
filesystem = path
try :
filesystem . check ( )
except fs . errors . FilesystemClosed :
raise UFOLibError ( " the filesystem ' %s ' is closed " % path )
else :
self . fs = filesystem
try :
path = filesystem . getsyspath ( " / " )
except fs . errors . NoSysPath :
# network or in-memory FS may not map to the local one
path = unicode ( filesystem )
# when user passed an already initialized fs instance, it is her
# responsibility to close it, thus UFOReader.close/__exit__ are no-op
self . _shouldClose = False
# default to a 'package' structure
self . _fileStructure = UFOFileStructure . PACKAGE
else :
raise TypeError (
" Expected a path string or fs.base.FS object, found ' %s ' "
% type ( path ) . __name__
)
2018-10-10 18:25:32 +01:00
self . _path = fsdecode ( path )
2018-07-04 14:44:05 +02:00
self . _validate = validate
2018-06-11 11:59:16 -05:00
self . readMetaInfo ( validate = validate )
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
2018-10-23 19:43:37 +01:00
def _get_path ( self ) :
import warnings
warnings . warn (
" The ' path ' attribute is deprecated; use the ' fs ' attribute instead " ,
DeprecationWarning ,
stacklevel = 2 ,
)
return self . _path
path = property ( _get_path , doc = " The path of the UFO (DEPRECATED). " )
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__. " )
2018-10-08 18:01:01 +01:00
def _get_fileStructure ( self ) :
return self . _fileStructure
2016-06-08 11:09:37 -04:00
2018-10-08 18:01:01 +01:00
fileStructure = property (
_get_fileStructure ,
doc = (
2018-10-23 19:43:37 +01:00
" The file structure of the UFO: "
2018-10-08 18:01:01 +01:00
" either UFOFileStructure.ZIP or UFOFileStructure.PACKAGE "
)
)
2016-06-08 11:09:37 -04:00
2011-09-27 17:58:45 +00:00
# up conversion
2018-06-11 11:07:29 -05:00
def _upConvertKerning ( self , validate ) :
2011-09-27 17:58:45 +00:00
"""
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 .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the data .
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 ( )
2018-06-11 11:07:29 -05:00
if validate :
invalidFormatMessage = " groups.plist is not properly formatted. "
if not isinstance ( groups , dict ) :
2011-10-03 15:17:31 +00:00
raise UFOLibError ( invalidFormatMessage )
2018-10-03 15:52:45 +01:00
for groupName , glyphList in groups . items ( ) :
2018-06-11 11:07:29 -05:00
if not isinstance ( groupName , basestring ) :
raise UFOLibError ( invalidFormatMessage )
elif not isinstance ( glyphList , list ) :
2011-10-03 15:17:31 +00:00
raise UFOLibError ( invalidFormatMessage )
2018-06-11 11:07:29 -05:00
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
2012-01-30 15:19:05 +00:00
kerning , groups , conversionMaps = convertUFO1OrUFO2KerningToUFO3Kerning (
2011-09-27 17:58:45 +00:00
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
2012-01-30 15:19:05 +00:00
self . _upConvertedKerningData [ " groupRenameMaps " ] = conversionMaps
2011-09-27 17:58:45 +00:00
2017-07-21 14:05:59 +01:00
# support methods
2018-10-23 19:43:37 +01:00
def readBytesFromPath ( self , path ) :
"""
Returns the bytes in the file at the given path .
The path must be relative to the UFO ' s filesystem root.
Returns None if the file does not exist .
"""
try :
return self . fs . getbytes ( fsdecode ( path ) )
except fs . errors . ResourceNotFound :
return None
2018-10-08 18:01:01 +01: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 .
By default the file is opened in binary mode ( reads bytes ) .
If encoding is passed , the file is opened in text mode ( reads unicode ) .
Note : The caller is responsible for closing the open file .
"""
2018-10-09 18:20:10 +01:00
path = fsdecode ( path )
2018-10-08 18:01:01 +01:00
try :
if encoding is None :
return self . fs . openbin ( path )
else :
return self . fs . open ( path , mode = " r " , encoding = encoding )
except fs . errors . ResourceNotFound :
return None
2011-09-12 17:49:34 +00:00
# metainfo.plist
2018-06-11 15:03:54 -05:00
def readMetaInfo ( self , validate = None ) :
2009-02-28 15:47:24 +00:00
"""
Read metainfo . plist . Only used for internal operations .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the read data , by default it is set
to the class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2018-10-08 18:01:01 +01:00
data = self . _getPlist ( METAINFO_FILENAME )
2018-06-12 17:40:25 -04:00
if validate and not isinstance ( data , dict ) :
2015-10-30 11:02:56 +00:00
raise UFOLibError ( " metainfo.plist is not properly formatted. " )
2009-02-28 15:47:24 +00:00
formatVersion = data [ " formatVersion " ]
2018-06-11 11:07:29 -05:00
if validate :
if not isinstance ( formatVersion , int ) :
2018-10-08 18:01:01 +01:00
raise UFOLibError (
" formatVersion must be specified as an integer in ' %s ' on %s "
% ( METAINFO_FILENAME , self . fs )
)
2018-06-11 11:07:29 -05:00
if formatVersion not in supportedUFOFormatVersions :
2018-10-08 18:01:01 +01:00
raise UFOLibError (
" Unsupported UFO format ( %d ) in ' %s ' on %s "
% ( formatVersion , METAINFO_FILENAME , self . fs )
)
2009-02-28 15:47:24 +00:00
self . _formatVersion = formatVersion
2011-09-12 17:49:34 +00:00
# groups.plist
2011-09-27 17:58:45 +00:00
def _readGroups ( self ) :
2018-10-08 18:01:01 +01:00
return self . _getPlist ( GROUPS_FILENAME , { } )
2011-09-27 17:58:45 +00:00
2018-06-11 15:03:54 -05:00
def readGroups ( self , validate = None ) :
2009-02-28 15:47:24 +00:00
"""
Read groups . plist . Returns a dict .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the read data , by default it is set to the
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2011-09-27 17:58:45 +00:00
# handle up conversion
if self . _formatVersion < 3 :
2018-06-11 11:07:29 -05:00
self . _upConvertKerning ( validate )
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 ( )
2018-06-11 11:07:29 -05:00
if validate :
valid , message = groupsValidator ( groups )
if not valid :
raise UFOLibError ( message )
2011-10-03 15:17:31 +00:00
return groups
2011-09-27 17:58:45 +00:00
2018-06-11 15:03:54 -05:00
def getKerningGroupConversionRenameMaps ( self , validate = None ) :
2012-01-30 15:19:05 +00:00
"""
Get maps defining the renaming that was done during any
needed kerning group conversion . This method returns a
dictionary of this form :
{
" side1 " : { " old group name " : " new group name " } ,
" side2 " : { " old group name " : " new group name " }
}
When no conversion has been performed , the side1 and side2
dictionaries will be empty .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the groups , by default it is set to the
class ' s validate value, can be overridden.
2012-01-30 15:19:05 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2012-01-30 15:19:05 +00:00
if self . _formatVersion > = 3 :
return dict ( side1 = { } , side2 = { } )
# use the public group reader to force the load and
# conversion of the data if it hasn't happened yet.
2018-06-11 11:07:29 -05:00
self . readGroups ( validate = validate )
2012-01-30 15:19:05 +00:00
return self . _upConvertedKerningData [ " groupRenameMaps " ]
2011-09-27 17:58:45 +00:00
# fontinfo.plist
2018-06-11 23:13:00 -05:00
def _readInfo ( self , validate ) :
2018-10-08 18:01:01 +01:00
data = self . _getPlist ( FONTINFO_FILENAME , { } )
2018-06-11 23:13:00 -05:00
if validate and not isinstance ( data , dict ) :
2011-09-28 14:47:25 +00:00
raise UFOLibError ( " fontinfo.plist is not properly formatted. " )
return data
2009-02-28 15:47:24 +00:00
2018-06-11 15:03:54 -05:00
def readInfo ( self , info , validate = None ) :
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 .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the read data , by default it is set to the
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2018-06-11 23:13:00 -05:00
infoDict = self . _readInfo ( validate )
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 )
2011-12-06 20:25:49 +00:00
infoDataToSet = _convertFontInfoDataVersion2ToVersion3 ( infoDataToSet )
2009-02-28 15:47:24 +00:00
# version 2
elif self . _formatVersion == 2 :
2015-11-05 09:03:19 +00:00
for attr , dataValidationDict in list ( fontInfoAttributesVersion2ValueData . items ( ) ) :
2009-02-28 15:47:24 +00:00
value = infoDict . get ( attr )
if value is None :
continue
infoDataToSet [ attr ] = value
2011-12-06 20:25:49 +00:00
infoDataToSet = _convertFontInfoDataVersion2ToVersion3 ( infoDataToSet )
2011-09-14 21:13:27 +00:00
# version 3
elif self . _formatVersion == 3 :
2015-11-05 09:03:19 +00:00
for attr , dataValidationDict in list ( 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
2018-06-11 11:07:29 -05:00
if validate :
infoDataToSet = validateInfoVersion3Data ( infoDataToSet )
2009-02-28 15:47:24 +00:00
# populate the object
2015-11-05 09:03:19 +00:00
for attr , value in list ( 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 ) :
2018-10-08 18:01:01 +01:00
data = self . _getPlist ( KERNING_FILENAME , { } )
2011-09-28 14:47:25 +00:00
return data
2011-09-27 17:58:45 +00:00
2018-06-11 15:03:54 -05:00
def readKerning ( self , validate = None ) :
2009-02-28 15:47:24 +00:00
"""
Read kerning . plist . Returns a dict .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the kerning data , by default it is set to the
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2011-09-27 17:58:45 +00:00
# handle up conversion
if self . _formatVersion < 3 :
2018-06-11 11:07:29 -05:00
self . _upConvertKerning ( validate )
2011-09-27 17:58:45 +00:00
kerningNested = self . _upConvertedKerningData [ " kerning " ]
# normal
else :
kerningNested = self . _readKerning ( )
2018-06-11 11:07:29 -05:00
if validate :
valid , message = kerningValidator ( kerningNested )
if not valid :
raise UFOLibError ( message )
2011-09-27 17:58:45 +00:00
# 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
2018-06-11 15:03:54 -05:00
def readLib ( self , validate = None ) :
2009-02-28 15:47:24 +00:00
"""
Read lib . plist . Returns a dict .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2018-10-08 18:01:01 +01:00
data = self . _getPlist ( LIB_FILENAME , { } )
2018-06-11 11:07:29 -05:00
if validate :
valid , message = fontLibValidator ( data )
if not valid :
raise UFOLibError ( message )
2011-10-03 15:56:08 +00:00
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 ) :
"""
2018-10-08 18:01:01 +01:00
Read features . fea . Return a unicode string .
The returned string is empty if the file is missing .
2009-02-28 15:47:24 +00:00
"""
2018-10-08 18:01:01 +01:00
try :
with self . fs . open ( FEATURES_FILENAME , " r " , encoding = " utf-8 " ) as f :
return f . read ( )
except fs . errors . ResourceNotFound :
2009-02-28 15:47:24 +00:00
return " "
2011-09-12 17:49:34 +00:00
# glyph sets & layers
2018-06-11 11:07:29 -05:00
def _readLayerContents ( self , validate ) :
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 .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the layer contents .
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 ) ]
2018-10-08 18:01:01 +01:00
contents = self . _getPlist ( LAYERCONTENTS_FILENAME )
2018-06-11 11:07:29 -05:00
if validate :
2018-10-08 18:01:01 +01:00
valid , error = layerContentsValidator ( contents , self . fs )
2018-06-11 11:07:29 -05: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
2018-06-11 15:03:54 -05:00
def getLayerNames ( self , validate = None ) :
2011-09-11 23:47:21 +00:00
"""
Get the ordered layer names from layercontents . plist .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2011-09-11 23:47:21 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2018-06-11 11:07:29 -05:00
layerContents = self . _readLayerContents ( validate )
2011-09-11 23:47:21 +00:00
layerNames = [ layerName for layerName , directoryName in layerContents ]
return layerNames
2018-06-11 15:03:54 -05:00
def getDefaultLayerName ( self , validate = None ) :
2011-09-11 23:47:21 +00:00
"""
Get the default layer name from layercontents . plist .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2011-09-11 23:47:21 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2018-06-11 11:07:29 -05:00
layerContents = self . _readLayerContents ( validate )
2011-09-11 23:47:21 +00:00
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. " )
2018-06-11 15:03:54 -05:00
def getGlyphSet ( self , layerName = None , validateRead = None , validateWrite = 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 .
2018-06-11 11:07:29 -05:00
2018-06-11 15:03:54 -05:00
` ` validateRead ` ` will validate the read data , by default it is set to the
class ' s validate value, can be overridden.
` ` validateWrte ` ` will validate the written data , by default it is set to the
2018-06-11 11:07:29 -05:00
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-10-17 17:41:43 +01:00
from fontTools . ufoLib . glifLib import GlyphSet
2018-10-09 14:13:28 +01:00
2018-06-11 15:03:54 -05:00
if validateRead is None :
validateRead = self . _validate
if validateWrite is None :
validateWrite = self . _validate
2011-09-11 23:47:21 +00:00
if layerName is None :
2018-06-11 15:03:54 -05:00
layerName = self . getDefaultLayerName ( validate = validateRead )
2011-09-11 23:47:21 +00:00
directory = None
2018-06-11 15:03:54 -05:00
layerContents = self . _readLayerContents ( validateRead )
2011-09-11 23:47:21 +00:00
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 )
2018-10-08 18:01:01 +01:00
try :
glyphSubFS = self . fs . opendir ( directory )
except fs . errors . ResourceNotFound :
raise UFOLibError (
" No ' %s ' directory for layer ' %s ' " % ( directory , layerName )
)
2018-10-03 15:52:45 +01:00
return GlyphSet (
2018-10-08 18:01:01 +01:00
glyphSubFS ,
2018-10-03 15:52:45 +01:00
ufoFormatVersion = self . _formatVersion ,
validateRead = validateRead ,
validateWrite = validateWrite ,
)
2008-01-07 17:40:34 +00:00
2018-06-11 15:03:54 -05:00
def getCharacterMapping ( self , layerName = None , validate = 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 .
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
glyphSet = self . getGlyphSet ( layerName , validateRead = validate , validateWrite = True )
2008-01-07 17:40:34 +00:00
allUnicodes = glyphSet . getUnicodes ( )
cmap = { }
2015-11-05 09:03:19 +00:00
for glyphName , unicodes in allUnicodes . items ( ) :
2008-01-07 17:40:34 +00:00
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
2016-05-02 23:06:25 -04:00
def getDataDirectoryListing ( self ) :
2011-09-12 11:35:57 +00:00
"""
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:35:57 +00:00
"""
2018-10-21 15:42:02 +01:00
try :
self . _dataFS = self . fs . opendir ( DATA_DIRNAME )
except fs . errors . ResourceNotFound :
return [ ]
except fs . errors . DirectoryExpected :
raise UFOLibError ( " The UFO contains a \" data \" file instead of a directory. " )
2018-10-08 18:01:01 +01:00
try :
# fs Walker.files method returns "absolute" paths (in terms of the
# root of the 'data' SubFS), so we strip the leading '/' to make
# them relative
return [
2018-10-21 15:42:02 +01:00
p . lstrip ( " / " ) for p in self . _dataFS . walk . files ( )
2018-10-08 18:01:01 +01:00
]
except fs . errors . ResourceError :
2011-09-12 11:35:57 +00:00
return [ ]
2018-06-11 15:03:54 -05:00
def getImageDirectoryListing ( self , validate = None ) :
2011-10-11 16:19:11 +00:00
"""
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 .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2011-10-11 16:19:11 +00:00
"""
2011-10-11 16:21:58 +00:00
if self . _formatVersion < 3 :
return [ ]
2018-10-03 15:52:45 +01:00
if validate is None :
validate = self . _validate
2018-10-21 15:42:02 +01:00
try :
self . _imagesFS = imagesFS = self . fs . opendir ( IMAGES_DIRNAME )
except fs . errors . ResourceNotFound :
2011-10-11 16:19:11 +00:00
return [ ]
2018-10-21 15:42:02 +01:00
except fs . errors . DirectoryExpected :
2011-10-11 16:19:11 +00:00
raise UFOLibError ( " The UFO contains an \" images \" file instead of a directory. " )
result = [ ]
2018-10-08 18:01:01 +01:00
for path in imagesFS . scandir ( " / " ) :
if path . is_dir :
2011-10-11 16:19:11 +00:00
# silently skip this as version control
# systems often have hidden directories
continue
2018-06-11 11:07:29 -05:00
if validate :
2018-10-08 18:01:01 +01:00
with imagesFS . openbin ( path . name ) as fp :
2018-10-03 15:52:45 +01:00
valid , error = pngValidator ( fileObj = fp )
2018-06-11 11:07:29 -05:00
if valid :
2018-10-08 18:01:01 +01:00
result . append ( path . name )
2018-07-04 14:44:35 +02:00
else :
2018-10-08 18:01:01 +01:00
result . append ( path . name )
2011-10-11 16:19:11 +00:00
return result
2018-10-21 15:42:02 +01:00
def readData ( self , fileName ) :
"""
Return bytes for the file named ' fileName ' inside the ' data/ ' directory .
"""
fileName = fsdecode ( fileName )
try :
try :
dataFS = self . _dataFS
except AttributeError :
# in case readData is called before getDataDirectoryListing
dataFS = self . fs . opendir ( DATA_DIRNAME )
data = dataFS . getbytes ( fileName )
except fs . errors . ResourceNotFound :
raise UFOLibError ( " No data file named ' %s ' on %s " % ( fileName , self . fs ) )
return data
2018-06-11 15:03:54 -05:00
def readImage ( self , fileName , validate = None ) :
2011-10-11 16:19:11 +00:00
"""
Return image data for the file named fileName .
2018-06-11 11:07:29 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2011-10-11 16:19:11 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2011-10-11 16:21:58 +00:00
if self . _formatVersion < 3 :
raise UFOLibError ( " Reading images is not allowed in UFO %d . " % self . _formatVersion )
2018-10-09 18:20:10 +01:00
fileName = fsdecode ( fileName )
2018-10-21 15:42:02 +01:00
try :
try :
imagesFS = self . _imagesFS
except AttributeError :
# in case readImage is called before getImageDirectoryListing
imagesFS = self . fs . opendir ( IMAGES_DIRNAME )
data = imagesFS . getbytes ( fileName )
except fs . errors . ResourceNotFound :
raise UFOLibError ( " No image file named ' %s ' on %s " % ( fileName , self . fs ) )
2018-06-11 11:07:29 -05:00
if validate :
valid , error = pngValidator ( data = data )
if not valid :
raise UFOLibError ( error )
2011-10-11 16:19:11 +00:00
return data
2011-09-12 13:25:24 +00:00
2018-10-08 18:01:01 +01:00
def close ( self ) :
if self . _shouldClose :
self . fs . close ( )
def __enter__ ( self ) :
return self
def __exit__ ( self , exc_type , exc_value , exc_tb ) :
self . close ( )
2016-06-08 09:36:57 -04:00
2009-02-28 15:47:24 +00:00
# ----------
# UFO Writer
# ----------
2018-10-23 20:41:35 +01:00
class UFOWriter ( UFOReader ) :
2009-02-28 15:47:24 +00:00
2018-06-11 11:59:16 -05:00
"""
Write the various components of the . ufo .
By default , the written data will be validated before writing . Set ` ` validate ` ` to
` ` False ` ` if you do not want to validate the data . Validation can also be overriden
on a per method level if desired .
"""
2009-02-28 15:47:24 +00:00
2018-10-03 15:52:45 +01:00
def __init__ (
self ,
path ,
formatVersion = 3 ,
2018-10-19 11:25:52 -05:00
fileCreator = " com.github.fonttools.ufoLib " ,
2018-10-03 15:52:45 +01:00
structure = None ,
validate = True ,
) :
2009-02-28 15:47:24 +00:00
if formatVersion not in supportedUFOFormatVersions :
raise UFOLibError ( " Unsupported UFO format ( %d ). " % formatVersion )
2018-10-08 18:01:01 +01:00
if hasattr ( path , " __fspath__ " ) : # support os.PathLike objects
path = path . __fspath__ ( )
if isinstance ( path , basestring ) :
2018-10-23 19:10:27 +01:00
# normalize path by removing trailing or double slashes
path = os . path . normpath ( path )
2018-10-08 18:01:01 +01:00
havePreviousFile = os . path . exists ( path )
if havePreviousFile :
# ensure we use the same structure as the destination
2018-10-10 09:57:02 +01:00
existingStructure = _sniffFileStructure ( path )
2018-10-08 18:01:01 +01:00
if structure is not None :
try :
structure = UFOFileStructure ( structure )
except ValueError :
raise UFOLibError (
" Invalid or unsupported structure: ' %s ' " % structure
)
if structure is not existingStructure :
raise UFOLibError (
" A UFO with a different structure ( %s ) already exists "
" at the given path: ' %s ' " % ( existingStructure , path )
)
else :
structure = existingStructure
else :
# if not exists, default to 'package' structure
if structure is None :
structure = UFOFileStructure . PACKAGE
dirName = os . path . dirname ( path )
if dirName and not os . path . isdir ( dirName ) :
raise UFOLibError (
" Cannot write to ' %s ' : directory does not exist " % path
)
if structure is UFOFileStructure . ZIP :
if havePreviousFile :
# we can't write a zip in-place, so we have to copy its
# contents to a temporary location and work from there, then
# upon closing UFOWriter we create the final zip file
parentFS = fs . tempfs . TempFS ( )
with fs . zipfs . ZipFS ( path , encoding = " utf-8 " ) as origFS :
fs . copy . copy_fs ( origFS , parentFS )
# if output path is an existing zip, we require that it contains
# one, and only one, root directory (with arbitrary name), in turn
# containing all the existing UFO contents
rootDirs = [
p . name for p in parentFS . scandir ( " / " )
# exclude macOS metadata contained in zip file
if p . is_dir and p . name != " __MACOSX "
]
if len ( rootDirs ) != 1 :
raise UFOLibError (
" Expected exactly 1 root directory, found %d " % len ( rootDirs )
)
else :
# 'ClosingSubFS' ensures that the parent filesystem is closed
# when its root subdirectory is closed
self . fs = parentFS . opendir (
rootDirs [ 0 ] , factory = fs . subfs . ClosingSubFS
)
else :
# if the output zip file didn't exist, we create the root folder;
# we name it the same as input 'path', but with '.ufo' extension
rootDir = os . path . splitext ( os . path . basename ( path ) ) [ 0 ] + " .ufo "
parentFS = fs . zipfs . ZipFS ( path , write = True , encoding = " utf-8 " )
parentFS . makedir ( rootDir )
self . fs = parentFS . opendir ( rootDir , factory = fs . subfs . ClosingSubFS )
else :
self . fs = fs . osfs . OSFS ( path , create = True )
self . _fileStructure = structure
self . _havePreviousFile = havePreviousFile
self . _shouldClose = True
elif isinstance ( path , fs . base . FS ) :
filesystem = path
try :
filesystem . check ( )
except fs . errors . FilesystemClosed :
raise UFOLibError ( " the filesystem ' %s ' is closed " % path )
else :
self . fs = filesystem
try :
path = filesystem . getsyspath ( " / " )
except fs . errors . NoSysPath :
# network or in-memory FS may not map to the local one
2018-10-09 14:20:52 +01:00
path = unicode ( filesystem )
2018-10-10 15:28:15 +01:00
# if passed an FS object, always use 'package' structure
if structure and structure is not UFOFileStructure . PACKAGE :
import warnings
warnings . warn (
" The ' structure ' argument is not used when input is an FS object " ,
UserWarning ,
stacklevel = 2 ,
)
2018-10-08 18:01:01 +01:00
self . _fileStructure = UFOFileStructure . PACKAGE
# if FS contains a "metainfo.plist", we consider it non-empty
self . _havePreviousFile = filesystem . exists ( METAINFO_FILENAME )
# the user is responsible for closing the FS object
self . _shouldClose = False
else :
raise TypeError (
" Expected a path string or fs object, found %s "
% type ( path ) . __name__
)
2011-09-27 15:28:42 +00:00
# establish some basic stuff
2018-10-10 18:25:32 +01:00
self . _path = fsdecode ( path )
2011-09-27 15:28:42 +00:00
self . _formatVersion = formatVersion
self . _fileCreator = fileCreator
2014-02-18 11:49:26 -05:00
self . _downConversionKerningData = None
2018-06-11 11:07:29 -05:00
self . _validate = validate
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
2018-10-09 17:16:55 +01:00
if self . _havePreviousFile :
2018-10-08 18:01:01 +01:00
metaInfo = self . _getPlist ( METAINFO_FILENAME )
2011-09-27 15:28:42 +00:00
previousFormatVersion = metaInfo . get ( " formatVersion " )
try :
previousFormatVersion = int ( previousFormatVersion )
2018-10-08 18:01:01 +01:00
except ( ValueError , TypeError ) :
self . fs . close ( )
2011-09-27 15:28:42 +00:00
raise UFOLibError ( " The existing metainfo.plist is not properly formatted. " )
if previousFormatVersion not in supportedUFOFormatVersions :
2018-10-08 18:01:01 +01:00
self . fs . close ( )
2011-09-27 15:28:42 +00:00
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 = { }
2015-09-27 16:39:30 +02:00
if previousFormatVersion is not None and previousFormatVersion > = 3 :
2011-09-26 19:57:10 +00:00
# already exists
2018-10-23 20:30:52 +01:00
self . layerContents = OrderedDict ( self . _readLayerContents ( validate ) )
2011-09-26 19:57:10 +00:00
else :
2011-09-19 01:40:21 +00:00
# previous < 3
# imply the layer contents
2018-10-08 18:01:01 +01:00
if self . fs . exists ( DEFAULT_GLYPHS_DIRNAME ) :
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
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__. " )
2018-10-08 18:01:01 +01:00
# support methods for file system interaction
2011-10-19 02:18:56 +00:00
def copyFromReader ( self , reader , sourcePath , destPath ) :
"""
Copy the sourcePath in the provided UFOReader to destPath
2018-10-08 18:01:01 +01:00
in this writer . The paths must be relative . This works with
both individual files and directories .
2011-10-19 02:18:56 +00:00
"""
if not isinstance ( reader , UFOReader ) :
raise UFOLibError ( " The reader must be an instance of UFOReader. " )
2018-10-09 18:20:10 +01:00
sourcePath = fsdecode ( sourcePath )
destPath = fsdecode ( destPath )
2018-10-08 18:01:01 +01:00
if not reader . fs . exists ( sourcePath ) :
2016-05-04 10:53:43 -04:00
raise UFOLibError ( " The reader does not have data located at \" %s \" . " % sourcePath )
2018-10-08 18:01:01 +01:00
if self . fs . exists ( destPath ) :
2016-05-04 10:53:43 -04:00
raise UFOLibError ( " A file named \" %s \" already exists. " % destPath )
2018-10-08 18:01:01 +01:00
# create the destination directory if it doesn't exist
self . fs . makedirs ( fs . path . dirname ( destPath ) , recreate = True )
if reader . fs . isdir ( sourcePath ) :
fs . copy . copy_dir ( reader . fs , sourcePath , self . fs , destPath )
2016-06-08 14:26:01 -04:00
else :
2018-10-08 18:01:01 +01:00
fs . copy . copy_file ( reader . fs , sourcePath , self . fs , destPath )
2016-06-08 14:26:01 -04:00
2018-10-08 18:01:01 +01:00
def writeBytesToPath ( self , path , data ) :
2016-06-08 11:09:37 -04:00
"""
2018-10-08 18:01:01 +01:00
Write bytes to a path relative to the UFO filesystem ' s root.
If writing to an existing UFO , check to see if data matches the data
that is already in the file at path ; if so , the file is not rewritten
so that the modification date is preserved .
If needed , the directory tree for the given path will be built .
"""
2018-10-09 18:20:10 +01:00
path = fsdecode ( path )
2018-10-08 18:01:01 +01:00
if self . _havePreviousFile :
if self . fs . isfile ( path ) and data == self . fs . getbytes ( path ) :
return
try :
self . fs . setbytes ( path , data )
except fs . errors . FileExpected :
raise UFOLibError ( " A directory exists at ' %s ' " % path )
except fs . errors . ResourceNotFound :
self . fs . makedirs ( fs . path . dirname ( path ) , recreate = True )
self . fs . setbytes ( path , data )
2016-06-08 11:09:37 -04:00
def getFileObjectForPath ( self , path , mode = " w " , 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
2018-10-08 18:01:01 +01:00
not exist and the mode is " r " or " rb.
An encoding may be passed if the file is opened in text mode .
2016-06-08 11:09:37 -04:00
Note : The caller is responsible for closing the open file .
"""
2018-10-09 18:20:10 +01:00
path = fsdecode ( path )
2018-10-08 18:01:01 +01:00
try :
return self . fs . open ( path , mode = mode , encoding = encoding )
except fs . errors . ResourceNotFound as e :
m = mode [ 0 ]
if m == " r " :
# XXX I think we should just let it raise. The docstring,
# however, says that this returns None if mode is 'r'
return None
elif m == " w " or m == " a " or m == " x " :
self . fs . makedirs ( fs . path . dirname ( path ) , recreate = True )
return self . fs . open ( path , mode = mode , encoding = encoding )
except fs . errors . ResourceError as e :
return UFOLibError (
" unable to open ' %s ' on %s : %s " % ( path , self . fs , e )
)
2016-06-08 11:09:37 -04:00
2018-10-08 18:01:01 +01:00
def removePath ( self , path , force = False , removeEmptyParents = True ) :
2016-06-08 11:09:37 -04:00
"""
Remove the file ( or directory ) at path . The path
must be relative to the UFO .
2018-10-08 18:01:01 +01:00
Raises UFOLibError if the path doesn ' t exist.
If force = True , ignore non - existent paths .
2018-10-09 15:12:19 +01:00
If the directory where ' path ' is located becomes empty , it will
be automatically removed , unless ' removeEmptyParents ' is False .
2018-10-08 18:01:01 +01:00
"""
2018-10-09 18:20:10 +01:00
path = fsdecode ( path )
2018-10-08 18:01:01 +01:00
try :
self . fs . remove ( path )
except fs . errors . FileExpected :
self . fs . removetree ( path )
except fs . errors . ResourceNotFound :
if not force :
raise UFOLibError (
" ' %s ' does not exist on %s " % ( path , self . fs )
)
if removeEmptyParents :
parent = fs . path . dirname ( path )
if parent :
fs . tools . remove_empty ( self . fs , parent )
# alias kept for backward compatibility with old API
removeFileForPath = removePath
2016-06-08 11:09:37 -04:00
2011-12-07 15:45:06 +00:00
# UFO mod time
def setModificationTime ( self ) :
"""
Set the UFO modification time to the current time .
This is never called automatically . It is up to the
caller to call this when finished working on the UFO .
"""
2018-10-09 18:20:10 +01:00
path = self . _path
2018-10-10 15:30:31 +01:00
if path is not None and os . path . exists ( path ) :
2018-10-10 10:54:08 +01:00
try :
# this may fail on some filesystems (e.g. SMB servers)
os . utime ( path , None )
except OSError as e :
logger . warning ( " Failed to set modified time: %s " , e )
2011-12-07 15:45:06 +00:00
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
metaInfo = dict (
creator = self . _fileCreator ,
formatVersion = self . _formatVersion
)
2018-10-08 18:01:01 +01:00
self . _writePlist ( METAINFO_FILENAME , metaInfo )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# groups.plist
2014-02-18 11:49:26 -05:00
def setKerningGroupConversionRenameMaps ( self , maps ) :
"""
Set maps defining the renaming that should be done
when writing groups and kerning in UFO 1 and UFO 2.
This will effectively undo the conversion done when
UFOReader reads this data . The dictionary should have
this form :
{
" side1 " : { " group name to use when writing " : " group name in data " } ,
" side2 " : { " group name to use when writing " : " group name in data " }
}
This is the same form returned by UFOReader ' s
getKerningGroupConversionRenameMaps method .
"""
if self . _formatVersion > = 3 :
return # XXX raise an error here
# flip the dictionaries
remap = { }
for side in ( " side1 " , " side2 " ) :
2015-11-05 09:03:19 +00:00
for writeName , dataName in list ( maps [ side ] . items ( ) ) :
2014-02-18 11:49:26 -05:00
remap [ dataName ] = writeName
self . _downConversionKerningData = dict ( groupRenameMap = remap )
2018-06-11 15:03:54 -05:00
def writeGroups ( self , groups , validate = None ) :
2009-02-28 15:47:24 +00:00
"""
Write groups . plist . This method requires a
dict of glyph groups as an argument .
2018-06-11 11:59:16 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2014-02-18 11:49:26 -05:00
# validate the data structure
2018-06-11 11:07:29 -05:00
if validate :
valid , message = groupsValidator ( groups )
if not valid :
raise UFOLibError ( message )
2014-02-18 11:49:26 -05:00
# down convert
if self . _formatVersion < 3 and self . _downConversionKerningData is not None :
remap = self . _downConversionKerningData [ " groupRenameMap " ]
remappedGroups = { }
# there are some edge cases here that are ignored:
# 1. if a group is being renamed to a name that
# already exists, the existing group is always
# overwritten. (this is why there are two loops
# below.) there doesn't seem to be a logical
# solution to groups mismatching and overwriting
# with the specifiecd group seems like a better
# solution than throwing an error.
# 2. if side 1 and side 2 groups are being renamed
# to the same group name there is no check to
# ensure that the contents are identical. that
# is left up to the caller.
2015-11-05 09:03:19 +00:00
for name , contents in list ( groups . items ( ) ) :
2014-02-18 11:49:26 -05:00
if name in remap :
continue
remappedGroups [ name ] = contents
2015-11-05 09:03:19 +00:00
for name , contents in list ( groups . items ( ) ) :
2014-02-18 11:49:26 -05:00
if name not in remap :
continue
name = remap [ name ]
remappedGroups [ name ] = contents
groups = remappedGroups
# pack and write
2008-01-07 17:40:34 +00:00
groupsNew = { }
2018-10-08 18:01:01 +01:00
for key , value in groups . items ( ) :
2008-01-07 17:40:34 +00:00
groupsNew [ key ] = list ( value )
if groupsNew :
2018-10-08 18:01:01 +01:00
self . _writePlist ( GROUPS_FILENAME , groupsNew )
elif self . _havePreviousFile :
self . removePath ( GROUPS_FILENAME , force = True , removeEmptyParents = False )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# fontinfo.plist
2018-06-11 15:03:54 -05:00
def writeInfo ( self , info , validate = None ) :
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 .
2018-06-11 11:59:16 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2011-09-14 21:13:27 +00:00
# gather version 3 data
2009-02-28 15:47:24 +00:00
infoData = { }
2015-11-05 09:03:19 +00:00
for attr in list ( 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 :
2018-06-11 11:07:29 -05:00
if validate :
infoData = validateInfoVersion3Data ( infoData )
2011-09-14 21:13:27 +00:00
elif self . _formatVersion == 2 :
infoData = _convertFontInfoDataVersion3ToVersion2 ( infoData )
2018-06-11 11:07:29 -05:00
if validate :
infoData = validateInfoVersion2Data ( infoData )
2011-09-14 21:13:27 +00:00
elif self . _formatVersion == 1 :
infoData = _convertFontInfoDataVersion3ToVersion2 ( infoData )
2018-06-11 11:07:29 -05:00
if validate :
infoData = validateInfoVersion2Data ( infoData )
2009-02-28 15:47:24 +00:00
infoData = _convertFontInfoDataVersion2ToVersion1 ( infoData )
# write file
2018-10-08 18:01:01 +01:00
self . _writePlist ( FONTINFO_FILENAME , infoData )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# kerning.plist
2018-06-11 15:03:54 -05:00
def writeKerning ( self , kerning , validate = None ) :
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 .
2018-06-11 11:59:16 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2014-02-18 11:49:26 -05:00
# validate the data structure
2018-06-11 11:07:29 -05:00
if validate :
invalidFormatMessage = " The kerning is not properly formatted. "
if not isDictEnough ( kerning ) :
2011-09-29 13:36:45 +00:00
raise UFOLibError ( invalidFormatMessage )
2018-06-11 11:07:29 -05:00
for pair , value in list ( 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 )
2018-10-10 10:42:06 +01:00
if not isinstance ( value , numberTypes ) :
2018-06-11 11:07:29 -05:00
raise UFOLibError ( invalidFormatMessage )
2014-02-18 11:49:26 -05:00
# down convert
if self . _formatVersion < 3 and self . _downConversionKerningData is not None :
remap = self . _downConversionKerningData [ " groupRenameMap " ]
remappedKerning = { }
2015-11-05 09:03:19 +00:00
for ( side1 , side2 ) , value in list ( kerning . items ( ) ) :
2014-02-18 11:49:26 -05:00
side1 = remap . get ( side1 , side1 )
side2 = remap . get ( side2 , side2 )
remappedKerning [ side1 , side2 ] = value
kerning = remappedKerning
# pack and write
2008-01-07 17:40:34 +00:00
kerningDict = { }
2018-10-08 18:01:01 +01:00
for left , right in kerning . keys ( ) :
2008-01-07 17:40:34 +00:00
value = kerning [ left , right ]
2018-04-03 17:36:41 -07:00
if left not in kerningDict :
2008-01-07 17:40:34 +00:00
kerningDict [ left ] = { }
kerningDict [ left ] [ right ] = value
if kerningDict :
2018-10-08 18:01:01 +01:00
self . _writePlist ( KERNING_FILENAME , kerningDict )
2018-10-09 17:16:55 +01:00
elif self . _havePreviousFile :
2018-10-08 18:01:01 +01:00
self . removePath ( KERNING_FILENAME , force = True , removeEmptyParents = False )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# lib.plist
2018-06-11 15:03:54 -05:00
def writeLib ( self , libDict , validate = None ) :
2009-02-28 15:47:24 +00:00
"""
Write lib . plist . This method requires a
lib dict as an argument .
2018-06-11 11:59:16 -05:00
` ` validate ` ` will validate the data , by default it is set to the
class ' s validate value, can be overridden.
2009-02-28 15:47:24 +00:00
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2018-06-11 11:59:16 -05:00
if validate :
valid , message = fontLibValidator ( libDict )
if not valid :
raise UFOLibError ( message )
2008-01-07 17:40:34 +00:00
if libDict :
2018-10-08 18:01:01 +01:00
self . _writePlist ( LIB_FILENAME , libDict )
2018-10-09 17:16:55 +01:00
elif self . _havePreviousFile :
2018-10-08 18:01:01 +01:00
self . removePath ( LIB_FILENAME , force = True , removeEmptyParents = False )
2008-01-07 17:40:34 +00:00
2011-09-12 17:49:34 +00:00
# features.fea
2018-06-11 23:13:00 -05:00
def writeFeatures ( self , features , validate = None ) :
2009-02-28 15:47:24 +00:00
"""
Write features . fea . This method requires a
features string as an argument .
"""
2018-06-11 23:13:00 -05:00
if validate is None :
validate = self . _validate
2009-02-28 15:47:24 +00:00
if self . _formatVersion == 1 :
raise UFOLibError ( " features.fea is not allowed in UFO Format Version 1. " )
2018-06-11 23:13:00 -05:00
if validate :
if not isinstance ( features , basestring ) :
raise UFOLibError ( " The features are not text. " )
2018-10-08 18:01:01 +01:00
if features :
self . writeBytesToPath ( FEATURES_FILENAME , features . encode ( " utf8 " ) )
2018-10-09 17:16:55 +01:00
elif self . _havePreviousFile :
2018-10-08 18:01:01 +01:00
self . removePath ( FEATURES_FILENAME , force = True , removeEmptyParents = False )
2009-02-28 15:47:24 +00:00
2011-09-12 17:49:34 +00:00
# glyph sets & layers
2018-06-11 23:13:00 -05:00
def writeLayerContents ( self , layerOrder = None , validate = 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 .
"""
2018-06-11 23:13:00 -05:00
if validate is None :
validate = self . _validate
2011-10-06 13:30:11 +00:00
if self . formatVersion < 3 :
return
2014-12-08 15:43:19 +01:00
if layerOrder is not None :
newOrder = [ ]
for layerName in layerOrder :
if layerName is None :
layerName = DEFAULT_LAYER_NAME
2018-07-15 21:32:13 +01:00
else :
layerName = tounicode ( layerName )
2014-12-08 15:43:19 +01:00
newOrder . append ( layerName )
layerOrder = newOrder
else :
2015-11-05 09:03:19 +00:00
layerOrder = list ( self . layerContents . keys ( ) )
2018-06-11 23:13:00 -05:00
if validate and set ( layerOrder ) != set ( self . layerContents . keys ( ) ) :
2018-06-12 17:40:25 -04:00
raise UFOLibError ( " The layer order content does not match the glyph sets that have been created. " )
2011-09-30 20:41:24 +00:00
layerContents = [ ( layerName , self . layerContents [ layerName ] ) for layerName in layerOrder ]
2018-10-08 18:01:01 +01:00
self . _writePlist ( LAYERCONTENTS_FILENAME , layerContents )
2008-01-07 17:40:34 +00:00
2011-09-26 19:57:10 +00:00
def _findDirectoryForLayerName ( self , layerName ) :
foundDirectory = None
2015-11-05 09:03:19 +00:00
for existingLayerName , directoryName in list ( self . layerContents . items ( ) ) :
2011-09-26 19:57:10 +00:00
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
2018-06-11 15:03:54 -05:00
def getGlyphSet ( self , layerName = None , defaultLayer = True , glyphNameToFileNameFunc = None , validateRead = None , validateWrite = 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 .
2018-06-11 11:59:16 -05:00
2018-06-11 15:03:54 -05:00
` ` validateRead ` ` will validate the read data , by default it is set to the
class ' s validate value, can be overridden.
` ` validateWrte ` ` will validate the written data , by default it is set to the
2018-06-11 11:59:16 -05:00
class ' s validate value, can be overridden.
2011-09-19 01:40:21 +00:00
"""
2018-06-11 15:03:54 -05:00
if validateRead is None :
validateRead = self . _validate
if validateWrite is None :
validateWrite = self . _validate
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 :
2018-10-23 20:30:52 +01:00
for existingLayerName , directory in self . layerContents . items ( ) :
2011-10-06 13:30:11 +00:00
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 :
2018-06-11 15:03:54 -05:00
return self . _getGlyphSetFormatVersion1 ( validateRead , validateWrite , glyphNameToFileNameFunc = glyphNameToFileNameFunc )
2011-10-06 13:30:11 +00:00
elif self . formatVersion == 2 :
2018-06-11 15:03:54 -05:00
return self . _getGlyphSetFormatVersion2 ( validateRead , validateWrite , glyphNameToFileNameFunc = glyphNameToFileNameFunc )
2011-10-06 13:30:11 +00:00
elif self . formatVersion == 3 :
2018-06-11 15:03:54 -05:00
return self . _getGlyphSetFormatVersion3 ( validateRead , validateWrite , layerName = layerName , defaultLayer = defaultLayer , glyphNameToFileNameFunc = glyphNameToFileNameFunc )
2018-10-08 18:01:01 +01:00
else :
raise AssertionError ( self . formatVersion )
2011-10-06 13:30:11 +00:00
2018-06-11 15:03:54 -05:00
def _getGlyphSetFormatVersion1 ( self , validateRead , validateWrite , glyphNameToFileNameFunc = None ) :
2018-10-17 17:41:43 +01:00
from fontTools . ufoLib . glifLib import GlyphSet
2018-10-09 14:13:28 +01:00
2018-10-08 18:01:01 +01:00
glyphSubFS = self . fs . makedir ( DEFAULT_GLYPHS_DIRNAME , recreate = True ) ,
2018-10-03 15:52:45 +01:00
return GlyphSet (
2018-10-08 18:01:01 +01:00
glyphSubFS ,
2018-10-03 15:52:45 +01:00
glyphNameToFileNameFunc = glyphNameToFileNameFunc ,
ufoFormatVersion = 1 ,
validateRead = validateRead ,
validateWrite = validateWrite ,
)
2011-10-06 13:30:11 +00:00
2018-06-11 15:03:54 -05:00
def _getGlyphSetFormatVersion2 ( self , validateRead , validateWrite , glyphNameToFileNameFunc = None ) :
2018-10-17 17:41:43 +01:00
from fontTools . ufoLib . glifLib import GlyphSet
2018-10-09 14:13:28 +01:00
2018-10-08 18:01:01 +01:00
glyphSubFS = self . fs . makedir ( DEFAULT_GLYPHS_DIRNAME , recreate = True )
2018-10-03 15:52:45 +01:00
return GlyphSet (
2018-10-08 18:01:01 +01:00
glyphSubFS ,
2018-10-03 15:52:45 +01:00
glyphNameToFileNameFunc = glyphNameToFileNameFunc ,
ufoFormatVersion = 2 ,
validateRead = validateRead ,
validateWrite = validateWrite ,
)
2011-10-06 13:30:11 +00:00
2018-06-11 15:03:54 -05:00
def _getGlyphSetFormatVersion3 ( self , validateRead , validateWrite , layerName = None , defaultLayer = True , glyphNameToFileNameFunc = None ) :
2018-10-17 17:41:43 +01:00
from fontTools . ufoLib . glifLib import GlyphSet
2018-10-09 14:13:28 +01:00
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 :
2018-10-23 20:30:52 +01:00
for existingLayerName , directory in self . layerContents . items ( ) :
2011-09-19 01:40:21 +00:00
if directory == DEFAULT_GLYPHS_DIRNAME :
2011-10-05 19:53:22 +00:00
if existingLayerName != layerName :
2018-10-24 12:37:37 +01:00
raise UFOLibError (
" Another layer ( ' %s ' ) is already mapped to the default directory. "
% existingLayerName
)
2011-10-05 19:53:22 +00:00
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
2018-10-23 20:30:52 +01:00
existing = { d . lower ( ) for d in self . layerContents . values ( ) }
2016-07-11 08:00:23 +01:00
if not isinstance ( layerName , unicode ) :
2011-09-19 01:40:21 +00:00
try :
2016-07-11 08:00:23 +01:00
layerName = unicode ( layerName )
2011-09-19 01:40:21 +00:00
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
2018-10-08 18:01:01 +01:00
glyphSubFS = self . fs . makedir ( directory , recreate = True )
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
2018-10-03 15:52:45 +01:00
return GlyphSet (
2018-10-08 18:01:01 +01:00
glyphSubFS ,
2018-10-03 15:52:45 +01:00
glyphNameToFileNameFunc = glyphNameToFileNameFunc ,
ufoFormatVersion = 3 ,
validateRead = validateRead ,
validateWrite = validateWrite ,
)
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 :
2017-09-26 21:58:26 +02:00
# ignore renaming glyph sets for UFO1 UFO2
# just write the data from the default layer
return
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
2018-10-23 20:30:52 +01:00
if defaultLayer and DEFAULT_GLYPHS_DIRNAME in self . layerContents . values ( ) :
2011-10-06 17:50:53 +00:00
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 :
2018-10-23 20:30:52 +01:00
existing = { name . lower ( ) for name in self . layerContents . values ( ) }
2011-10-06 16:09:40 +00:00
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
2018-10-08 18:01:01 +01:00
self . fs . movedir ( oldDirectory , newDirectory , create = True )
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 :
2017-09-26 21:58:26 +02:00
# ignore deleting glyph sets for UFO1 UFO2 as there are no layers
# just write the data from the default layer
return
2011-09-27 15:28:42 +00:00
foundDirectory = self . _findDirectoryForLayerName ( layerName )
2018-10-08 18:01:01 +01:00
self . removePath ( foundDirectory , removeEmptyParents = False )
2011-09-19 01:40:21 +00:00
del self . layerContents [ layerName ]
2009-02-28 15:47:24 +00:00
2018-10-21 15:42:02 +01:00
def writeData ( self , fileName , data ) :
"""
Write data to fileName in the ' data ' directory .
The data must be a bytes string .
"""
self . writeBytesToPath ( " %s / %s " % ( DATA_DIRNAME , fsdecode ( fileName ) ) , data )
def removeData ( self , fileName ) :
"""
Remove the file named fileName from the data directory .
"""
self . removePath ( " %s / %s " % ( DATA_DIRNAME , fsdecode ( fileName ) ) )
2011-10-11 16:19:11 +00:00
# /images
2018-06-11 15:03:54 -05:00
def writeImage ( self , fileName , data , validate = None ) :
2011-10-11 16:19:11 +00:00
"""
Write data to fileName in the images directory .
The data must be a valid PNG .
"""
2018-06-11 15:03:54 -05:00
if validate is None :
validate = self . _validate
2011-10-11 16:21:58 +00:00
if self . _formatVersion < 3 :
raise UFOLibError ( " Images are not allowed in UFO %d . " % self . _formatVersion )
2018-10-09 18:20:10 +01:00
fileName = fsdecode ( fileName )
2018-06-11 11:59:16 -05:00
if validate :
valid , error = pngValidator ( data = data )
if not valid :
raise UFOLibError ( error )
2018-10-08 18:01:01 +01:00
self . writeBytesToPath ( " %s / %s " % ( IMAGES_DIRNAME , fileName ) , data )
2011-10-11 16:19:11 +00:00
2018-10-08 18:01:01 +01:00
def removeImage ( self , fileName , validate = None ) : # XXX remove unused 'validate'?
2011-10-11 16:19:11 +00:00
"""
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 )
2018-10-09 18:20:10 +01:00
self . removePath ( " %s / %s " % ( IMAGES_DIRNAME , fsdecode ( fileName ) ) )
2011-10-11 16:19:11 +00:00
2018-06-11 23:13:00 -05:00
def copyImageFromReader ( self , reader , sourceFileName , destFileName , validate = None ) :
2011-10-19 14:58:35 +00:00
"""
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 .
"""
2018-06-11 23:13:00 -05:00
if validate is None :
validate = self . _validate
2011-10-19 14:58:35 +00:00
if self . _formatVersion < 3 :
raise UFOLibError ( " Images are not allowed in UFO %d . " % self . _formatVersion )
2018-10-09 18:20:10 +01:00
sourcePath = " %s / %s " % ( IMAGES_DIRNAME , fsdecode ( sourceFileName ) )
destPath = " %s / %s " % ( IMAGES_DIRNAME , fsdecode ( destFileName ) )
2011-10-19 14:58:35 +00:00
self . copyFromReader ( reader , sourcePath , destPath )
2018-10-08 18:01:01 +01:00
def close ( self ) :
if self . _havePreviousFile and self . _fileStructure is UFOFileStructure . ZIP :
# if we are updating an existing zip file, we can now compress the
# contents of the temporary filesystem in the destination path
rootDir = os . path . splitext ( os . path . basename ( self . _path ) ) [ 0 ] + " .ufo "
with fs . zipfs . ZipFS ( self . _path , write = True , encoding = " utf-8 " ) as destFS :
fs . copy . copy_fs ( self . fs , destFS . makedir ( rootDir ) )
if self . _shouldClose :
self . fs . close ( )
def __enter__ ( self ) :
return self
def __exit__ ( self , exc_type , exc_value , exc_tb ) :
self . close ( )
2011-10-19 14:58:35 +00:00
2018-10-23 20:40:53 +01:00
# just an alias, makes it more explicit
UFOReaderWriter = UFOWriter
2009-02-28 15:47:24 +00:00
# ----------------
# Helper Functions
# ----------------
2018-10-10 09:57:02 +01:00
def _sniffFileStructure ( ufo_path ) :
""" Return UFOFileStructure.ZIP if the UFO at path ' ufo_path ' (basestring)
is a zip file , else return UFOFileStructure . PACKAGE if ' ufo_path ' is a
directory .
Raise UFOLibError if it is a file with unknown structure , or if the path
does not exist .
"""
if zipfile . is_zipfile ( ufo_path ) :
return UFOFileStructure . ZIP
elif os . path . isdir ( ufo_path ) :
return UFOFileStructure . PACKAGE
elif os . path . isfile ( ufo_path ) :
raise UFOLibError (
" The specified UFO does not have a known structure: ' %s ' " % ufo_path
)
else :
raise UFOLibError ( " No such file or directory: ' %s ' " % ufo_path )
2009-02-28 15:47:24 +00:00
def makeUFOPath ( path ) :
"""
Return a . ufo pathname .
2016-09-11 17:44:12 +01:00
>> > makeUFOPath ( " directory/something.ext " ) == (
. . . os . path . join ( ' directory ' , ' something.ufo ' ) )
True
>> > makeUFOPath ( " directory/something.another.thing.ext " ) == (
. . . os . path . join ( ' directory ' , ' something.another.thing.ufo ' ) )
True
2009-02-28 15:47:24 +00:00
"""
dir , name = os . path . split ( path )
name = " . " . join ( [ " . " . join ( name . split ( " . " ) [ : - 1 ] ) , " ufo " ] )
return os . path . join ( dir , name )
# ----------------------
# 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 = { }
2015-11-05 09:03:19 +00:00
for attr , value in list ( infoData . items ( ) ) :
2009-02-28 15:47:24 +00:00
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 = { }
2015-11-05 09:03:19 +00:00
for attr , value in list ( infoData . items ( ) ) :
2011-09-14 21:13:27 +00:00
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
2015-11-05 09:03:19 +00:00
fontInfoOpenTypeHeadFlagsOptions = list ( range ( 0 , 15 ) )
2011-10-13 00:41:58 +00:00
fontInfoOpenTypeOS2SelectionOptions = [ 1 , 2 , 3 , 4 , 7 , 8 , 9 ]
2015-11-05 09:03:19 +00:00
fontInfoOpenTypeOS2UnicodeRangesOptions = list ( range ( 0 , 128 ) )
fontInfoOpenTypeOS2CodePageRangesOptions = list ( range ( 0 , 64 ) )
2011-09-18 12:24:29 +00:00
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 = {
2015-11-02 15:49:24 +00:00
" familyName " : dict ( type = basestring ) ,
" styleName " : dict ( type = basestring ) ,
" styleMapFamilyName " : dict ( type = basestring ) ,
" 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 ) ,
2015-11-02 15:49:24 +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 ) ) ,
2015-11-02 15:49:24 +00:00
" note " : dict ( type = basestring ) ,
" 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 ) ) ,
2015-11-02 15:49:24 +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 ) ,
2015-11-02 15:49:24 +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 ) ) ,
2015-11-02 15:49:24 +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 ) ) ,
2015-11-02 15:49:24 +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 ) ,
2015-11-02 15:49:24 +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 ) ,
2012-02-14 03:25:21 +00:00
" openTypeHeadLowestRecPPEM " : dict ( type = int , valueValidator = genericNonNegativeNumberValidator ) ,
" openTypeHheaAscender " : dict ( type = int ) ,
" openTypeHheaDescender " : dict ( type = int ) ,
" openTypeHheaLineGap " : dict ( type = int ) ,
" openTypeHheaCaretOffset " : dict ( type = int ) ,
2011-09-18 12:24:29 +00:00
" openTypeOS2Panose " : dict ( type = " integerList " , valueValidator = fontInfoVersion3OpenTypeOS2PanoseValidator ) ,
2012-02-14 03:25:21 +00:00
" openTypeOS2TypoAscender " : dict ( type = int ) ,
" openTypeOS2TypoDescender " : dict ( type = int ) ,
" openTypeOS2TypoLineGap " : dict ( type = int ) ,
" openTypeOS2WinAscent " : dict ( type = int , valueValidator = genericNonNegativeNumberValidator ) ,
" openTypeOS2WinDescent " : dict ( type = int , valueValidator = genericNonNegativeNumberValidator ) ,
" openTypeOS2SubscriptXSize " : dict ( type = int ) ,
" openTypeOS2SubscriptYSize " : dict ( type = int ) ,
" openTypeOS2SubscriptXOffset " : dict ( type = int ) ,
" openTypeOS2SubscriptYOffset " : dict ( type = int ) ,
" openTypeOS2SuperscriptXSize " : dict ( type = int ) ,
" openTypeOS2SuperscriptYSize " : dict ( type = int ) ,
" openTypeOS2SuperscriptXOffset " : dict ( type = int ) ,
" openTypeOS2SuperscriptYOffset " : dict ( type = int ) ,
" openTypeOS2StrikeoutSize " : dict ( type = int ) ,
" openTypeOS2StrikeoutPosition " : dict ( type = int ) ,
2011-09-18 12:24:29 +00:00
" openTypeGaspRangeRecords " : dict ( type = " dictList " , valueValidator = fontInfoOpenTypeGaspRangeRecordsValidator ) ,
" openTypeNameRecords " : dict ( type = " dictList " , valueValidator = fontInfoOpenTypeNameRecordsValidator ) ,
2012-02-14 03:25:21 +00:00
" openTypeVheaVertTypoAscender " : dict ( type = int ) ,
" openTypeVheaVertTypoDescender " : dict ( type = int ) ,
" openTypeVheaVertTypoLineGap " : dict ( type = int ) ,
" openTypeVheaCaretOffset " : dict ( type = int ) ,
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.
2015-11-05 09:03:19 +00:00
for attr , dataDict in list ( 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
2015-11-05 09:03:19 +00:00
for attr , dataDict in list ( 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 = { }
2015-11-05 09:03:19 +00:00
for key , value in list ( d . items ( ) ) :
2009-02-28 15:47:24 +00:00
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 )
2011-12-06 20:25:49 +00:00
# 1 <-> 2
2009-02-28 15:47:24 +00:00
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 = { }
2015-11-05 09:03:19 +00:00
for attr , value in list ( data . items ( ) ) :
2009-02-28 15:47:24 +00:00
# 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 = { }
2015-11-05 09:03:19 +00:00
for attr , value in list ( data . items ( ) ) :
2009-02-28 15:47:24 +00:00
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-12-06 20:25:49 +00:00
# 2 <-> 3
_ufo2To3NonNegativeInt = set ( (
2012-02-14 03:25:21 +00:00
" versionMinor " ,
2011-12-06 20:25:49 +00:00
" openTypeHeadLowestRecPPEM " ,
" openTypeOS2WinAscent " ,
" openTypeOS2WinDescent "
) )
2012-02-14 03:25:21 +00:00
_ufo2To3NonNegativeIntOrFloat = set ( (
" unitsPerEm "
) )
_ufo2To3FloatToInt = set ( ( (
" openTypeHeadLowestRecPPEM " ,
" openTypeHheaAscender " ,
" openTypeHheaDescender " ,
" openTypeHheaLineGap " ,
" openTypeHheaCaretOffset " ,
" openTypeOS2TypoAscender " ,
" openTypeOS2TypoDescender " ,
" openTypeOS2TypoLineGap " ,
" openTypeOS2WinAscent " ,
" openTypeOS2WinDescent " ,
" openTypeOS2SubscriptXSize " ,
" openTypeOS2SubscriptYSize " ,
" openTypeOS2SubscriptXOffset " ,
" openTypeOS2SubscriptYOffset " ,
" openTypeOS2SuperscriptXSize " ,
" openTypeOS2SuperscriptYSize " ,
" openTypeOS2SuperscriptXOffset " ,
" openTypeOS2SuperscriptYOffset " ,
" openTypeOS2StrikeoutSize " ,
" openTypeOS2StrikeoutPosition " ,
" openTypeVheaVertTypoAscender " ,
" openTypeVheaVertTypoDescender " ,
" openTypeVheaVertTypoLineGap " ,
" openTypeVheaCaretOffset "
) ) )
2011-12-06 20:25:49 +00:00
def convertFontInfoValueForAttributeFromVersion2ToVersion3 ( attr , value ) :
"""
Convert value from version 2 to version 3 format .
Returns the new attribute name and the converted value .
If the value is None , None will be returned for the new value .
"""
2012-02-14 03:25:21 +00:00
if attr in _ufo2To3FloatToInt :
try :
v = int ( round ( value ) )
except ( ValueError , TypeError ) :
raise UFOLibError ( " Could not convert value for %s . " % attr )
if v != value :
value = v
2011-12-06 20:25:49 +00:00
if attr in _ufo2To3NonNegativeInt :
try :
v = int ( abs ( value ) )
except ( ValueError , TypeError ) :
raise UFOLibError ( " Could not convert value for %s . " % attr )
if v != value :
value = v
elif attr in _ufo2To3NonNegativeIntOrFloat :
try :
v = float ( abs ( value ) )
except ( ValueError , TypeError ) :
raise UFOLibError ( " Could not convert value for %s . " % attr )
if v == int ( v ) :
v = int ( v )
if v != value :
value = v
return attr , value
def convertFontInfoValueForAttributeFromVersion3ToVersion2 ( attr , value ) :
"""
Convert value from version 3 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 .
"""
return attr , value
2011-09-14 21:13:27 +00:00
def _convertFontInfoDataVersion3ToVersion2 ( data ) :
converted = { }
2015-11-05 09:03:19 +00:00
for attr , value in list ( data . items ( ) ) :
2011-12-06 20:25:49 +00:00
newAttr , newValue = convertFontInfoValueForAttributeFromVersion3ToVersion2 ( attr , value )
if newAttr not in fontInfoAttributesVersion2 :
2011-09-14 21:13:27 +00:00
continue
2011-12-06 20:25:49 +00:00
converted [ newAttr ] = newValue
return converted
def _convertFontInfoDataVersion2ToVersion3 ( data ) :
converted = { }
2015-11-05 09:03:19 +00:00
for attr , value in list ( data . items ( ) ) :
2011-12-06 20:25:49 +00:00
attr , value = convertFontInfoValueForAttributeFromVersion2ToVersion3 ( attr , value )
2011-09-14 21:13:27 +00:00
converted [ attr ] = value
return converted
2009-02-28 15:47:24 +00:00
if __name__ == " __main__ " :
import doctest
doctest . testmod ( )