Home | History | Annotate | Download | only in misc
      1 """
      2 User name to file name conversion based on the UFO 3 spec:
      3 http://unifiedfontobject.org/versions/ufo3/conventions/
      4 
      5 The code was copied from:
      6 https://github.com/unified-font-object/ufoLib/blob/8747da7/Lib/ufoLib/filenames.py
      7 
      8 Author: Tal Leming
      9 Copyright (c) 2005-2016, The RoboFab Developers:
     10 	Erik van Blokland
     11 	Tal Leming
     12 	Just van Rossum
     13 """
     14 from __future__ import unicode_literals
     15 from fontTools.misc.py23 import basestring, unicode
     16 
     17 
     18 illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ")
     19 illegalCharacters += [chr(i) for i in range(1, 32)]
     20 illegalCharacters += [chr(0x7F)]
     21 reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ")
     22 reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ")
     23 maxFileNameLength = 255
     24 
     25 
     26 class NameTranslationError(Exception):
     27 	pass
     28 
     29 
     30 def userNameToFileName(userName, existing=[], prefix="", suffix=""):
     31 	"""
     32 	existing should be a case-insensitive list
     33 	of all existing file names.
     34 
     35 	>>> userNameToFileName("a") == "a"
     36 	True
     37 	>>> userNameToFileName("A") == "A_"
     38 	True
     39 	>>> userNameToFileName("AE") == "A_E_"
     40 	True
     41 	>>> userNameToFileName("Ae") == "A_e"
     42 	True
     43 	>>> userNameToFileName("ae") == "ae"
     44 	True
     45 	>>> userNameToFileName("aE") == "aE_"
     46 	True
     47 	>>> userNameToFileName("a.alt") == "a.alt"
     48 	True
     49 	>>> userNameToFileName("A.alt") == "A_.alt"
     50 	True
     51 	>>> userNameToFileName("A.Alt") == "A_.A_lt"
     52 	True
     53 	>>> userNameToFileName("A.aLt") == "A_.aL_t"
     54 	True
     55 	>>> userNameToFileName(u"A.alT") == "A_.alT_"
     56 	True
     57 	>>> userNameToFileName("T_H") == "T__H_"
     58 	True
     59 	>>> userNameToFileName("T_h") == "T__h"
     60 	True
     61 	>>> userNameToFileName("t_h") == "t_h"
     62 	True
     63 	>>> userNameToFileName("F_F_I") == "F__F__I_"
     64 	True
     65 	>>> userNameToFileName("f_f_i") == "f_f_i"
     66 	True
     67 	>>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash"
     68 	True
     69 	>>> userNameToFileName(".notdef") == "_notdef"
     70 	True
     71 	>>> userNameToFileName("con") == "_con"
     72 	True
     73 	>>> userNameToFileName("CON") == "C_O_N_"
     74 	True
     75 	>>> userNameToFileName("con.alt") == "_con.alt"
     76 	True
     77 	>>> userNameToFileName("alt.con") == "alt._con"
     78 	True
     79 	"""
     80 	# the incoming name must be a unicode string
     81 	if not isinstance(userName, unicode):
     82 		raise ValueError("The value for userName must be a unicode string.")
     83 	# establish the prefix and suffix lengths
     84 	prefixLength = len(prefix)
     85 	suffixLength = len(suffix)
     86 	# replace an initial period with an _
     87 	# if no prefix is to be added
     88 	if not prefix and userName[0] == ".":
     89 		userName = "_" + userName[1:]
     90 	# filter the user name
     91 	filteredUserName = []
     92 	for character in userName:
     93 		# replace illegal characters with _
     94 		if character in illegalCharacters:
     95 			character = "_"
     96 		# add _ to all non-lower characters
     97 		elif character != character.lower():
     98 			character += "_"
     99 		filteredUserName.append(character)
    100 	userName = "".join(filteredUserName)
    101 	# clip to 255
    102 	sliceLength = maxFileNameLength - prefixLength - suffixLength
    103 	userName = userName[:sliceLength]
    104 	# test for illegal files names
    105 	parts = []
    106 	for part in userName.split("."):
    107 		if part.lower() in reservedFileNames:
    108 			part = "_" + part
    109 		parts.append(part)
    110 	userName = ".".join(parts)
    111 	# test for clash
    112 	fullName = prefix + userName + suffix
    113 	if fullName.lower() in existing:
    114 		fullName = handleClash1(userName, existing, prefix, suffix)
    115 	# finished
    116 	return fullName
    117 
    118 def handleClash1(userName, existing=[], prefix="", suffix=""):
    119 	"""
    120 	existing should be a case-insensitive list
    121 	of all existing file names.
    122 
    123 	>>> prefix = ("0" * 5) + "."
    124 	>>> suffix = "." + ("0" * 10)
    125 	>>> existing = ["a" * 5]
    126 
    127 	>>> e = list(existing)
    128 	>>> handleClash1(userName="A" * 5, existing=e,
    129 	...		prefix=prefix, suffix=suffix) == (
    130 	... 	'00000.AAAAA000000000000001.0000000000')
    131 	True
    132 
    133 	>>> e = list(existing)
    134 	>>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
    135 	>>> handleClash1(userName="A" * 5, existing=e,
    136 	...		prefix=prefix, suffix=suffix) == (
    137 	... 	'00000.AAAAA000000000000002.0000000000')
    138 	True
    139 
    140 	>>> e = list(existing)
    141 	>>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
    142 	>>> handleClash1(userName="A" * 5, existing=e,
    143 	...		prefix=prefix, suffix=suffix) == (
    144 	... 	'00000.AAAAA000000000000001.0000000000')
    145 	True
    146 	"""
    147 	# if the prefix length + user name length + suffix length + 15 is at
    148 	# or past the maximum length, silce 15 characters off of the user name
    149 	prefixLength = len(prefix)
    150 	suffixLength = len(suffix)
    151 	if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength:
    152 		l = (prefixLength + len(userName) + suffixLength + 15)
    153 		sliceLength = maxFileNameLength - l
    154 		userName = userName[:sliceLength]
    155 	finalName = None
    156 	# try to add numbers to create a unique name
    157 	counter = 1
    158 	while finalName is None:
    159 		name = userName + str(counter).zfill(15)
    160 		fullName = prefix + name + suffix
    161 		if fullName.lower() not in existing:
    162 			finalName = fullName
    163 			break
    164 		else:
    165 			counter += 1
    166 		if counter >= 999999999999999:
    167 			break
    168 	# if there is a clash, go to the next fallback
    169 	if finalName is None:
    170 		finalName = handleClash2(existing, prefix, suffix)
    171 	# finished
    172 	return finalName
    173 
    174 def handleClash2(existing=[], prefix="", suffix=""):
    175 	"""
    176 	existing should be a case-insensitive list
    177 	of all existing file names.
    178 
    179 	>>> prefix = ("0" * 5) + "."
    180 	>>> suffix = "." + ("0" * 10)
    181 	>>> existing = [prefix + str(i) + suffix for i in range(100)]
    182 
    183 	>>> e = list(existing)
    184 	>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
    185 	... 	'00000.100.0000000000')
    186 	True
    187 
    188 	>>> e = list(existing)
    189 	>>> e.remove(prefix + "1" + suffix)
    190 	>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
    191 	... 	'00000.1.0000000000')
    192 	True
    193 
    194 	>>> e = list(existing)
    195 	>>> e.remove(prefix + "2" + suffix)
    196 	>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
    197 	... 	'00000.2.0000000000')
    198 	True
    199 	"""
    200 	# calculate the longest possible string
    201 	maxLength = maxFileNameLength - len(prefix) - len(suffix)
    202 	maxValue = int("9" * maxLength)
    203 	# try to find a number
    204 	finalName = None
    205 	counter = 1
    206 	while finalName is None:
    207 		fullName = prefix + str(counter) + suffix
    208 		if fullName.lower() not in existing:
    209 			finalName = fullName
    210 			break
    211 		else:
    212 			counter += 1
    213 		if counter >= maxValue:
    214 			break
    215 	# raise an error if nothing has been found
    216 	if finalName is None:
    217 		raise NameTranslationError("No unique name could be found.")
    218 	# finished
    219 	return finalName
    220 
    221 if __name__ == "__main__":
    222 	import doctest
    223 	import sys
    224 	sys.exit(doctest.testmod().failed)
    225