__init__.py 91 KB

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