| 1 | import os |
|---|
| 2 | import re |
|---|
| 3 | import weakref |
|---|
| 4 | from copy import deepcopy |
|---|
| 5 | import tempfile |
|---|
| 6 | import shutil |
|---|
| 7 | from fontTools.misc.arrayTools import unionRect |
|---|
| 8 | from ufoLib import UFOReader, UFOWriter |
|---|
| 9 | from ufoLib.validators import kerningValidator |
|---|
| 10 | from defcon.errors import DefconError |
|---|
| 11 | from defcon.objects.base import BaseObject |
|---|
| 12 | from defcon.objects.layerSet import LayerSet |
|---|
| 13 | from defcon.objects.layer import Layer |
|---|
| 14 | from defcon.objects.info import Info |
|---|
| 15 | from defcon.objects.kerning import Kerning |
|---|
| 16 | from defcon.objects.groups import Groups |
|---|
| 17 | from defcon.objects.features import Features |
|---|
| 18 | from defcon.objects.lib import Lib |
|---|
| 19 | from defcon.objects.imageSet import ImageSet |
|---|
| 20 | from defcon.objects.dataSet import DataSet |
|---|
| 21 | from defcon.tools.notifications import NotificationCenter |
|---|
| 22 | |
|---|
| 23 | |
|---|
| 24 | class Font(BaseObject): |
|---|
| 25 | |
|---|
| 26 | """ |
|---|
| 27 | If loading from an existing UFO, **path** should be the path to the UFO. |
|---|
| 28 | |
|---|
| 29 | If you subclass one of the sub objects, such as :class:`Glyph`, |
|---|
| 30 | the class must be registered when the font is created for defcon |
|---|
| 31 | to know about it. The **\*Class** arguments allow for individual |
|---|
| 32 | ovverrides. If None is provided for an argument, the defcon |
|---|
| 33 | appropriate class will be used. |
|---|
| 34 | |
|---|
| 35 | **This object posts the following notifications:** |
|---|
| 36 | |
|---|
| 37 | ====================== |
|---|
| 38 | Name |
|---|
| 39 | ====================== |
|---|
| 40 | Font.Changed |
|---|
| 41 | Font.ReloadedGlyphs |
|---|
| 42 | Font.GlyphOrderChanged |
|---|
| 43 | ====================== |
|---|
| 44 | |
|---|
| 45 | The Font object has some dict like behavior. For example, to get a glyph:: |
|---|
| 46 | |
|---|
| 47 | glyph = font["aGlyphName"] |
|---|
| 48 | |
|---|
| 49 | To iterate over all glyphs:: |
|---|
| 50 | |
|---|
| 51 | for glyph in font: |
|---|
| 52 | |
|---|
| 53 | To get the number of glyphs:: |
|---|
| 54 | |
|---|
| 55 | glyphCount = len(font) |
|---|
| 56 | |
|---|
| 57 | To find out if a font contains a particular glyph:: |
|---|
| 58 | |
|---|
| 59 | exists = "aGlyphName" in font |
|---|
| 60 | |
|---|
| 61 | To remove a glyph:: |
|---|
| 62 | |
|---|
| 63 | del font["aGlyphName"] |
|---|
| 64 | """ |
|---|
| 65 | |
|---|
| 66 | changeNotificationName = "Font.Changed" |
|---|
| 67 | representationFactories = {} |
|---|
| 68 | |
|---|
| 69 | def __init__(self, path=None, |
|---|
| 70 | kerningClass=None, infoClass=None, groupsClass=None, featuresClass=None, libClass=None, unicodeDataClass=None, |
|---|
| 71 | layerSetClass=None, layerClass=None, imageSetClass=None, dataSetClass=None, |
|---|
| 72 | guidelineClass=None, |
|---|
| 73 | glyphClass=None, glyphContourClass=None, glyphPointClass=None, glyphComponentClass=None, glyphAnchorClass=None, glyphImageClass=None): |
|---|
| 74 | |
|---|
| 75 | super(Font, self).__init__() |
|---|
| 76 | self._dispatcher = NotificationCenter() |
|---|
| 77 | self.beginSelfNotificationObservation() |
|---|
| 78 | |
|---|
| 79 | if infoClass is None: |
|---|
| 80 | infoClass = Info |
|---|
| 81 | if kerningClass is None: |
|---|
| 82 | kerningClass = Kerning |
|---|
| 83 | if groupsClass is None: |
|---|
| 84 | groupsClass = Groups |
|---|
| 85 | if featuresClass is None: |
|---|
| 86 | featuresClass = Features |
|---|
| 87 | if libClass is None: |
|---|
| 88 | libClass = Lib |
|---|
| 89 | if layerSetClass is None: |
|---|
| 90 | layerSetClass = LayerSet |
|---|
| 91 | if imageSetClass is None: |
|---|
| 92 | imageSetClass = ImageSet |
|---|
| 93 | if dataSetClass is None: |
|---|
| 94 | dataSetClass = DataSet |
|---|
| 95 | self._unicodeDataClass = unicodeDataClass |
|---|
| 96 | self._layerSetClass = layerSetClass |
|---|
| 97 | self._layerClass = layerClass |
|---|
| 98 | self._glyphClass = glyphClass |
|---|
| 99 | self._glyphContourClass = glyphContourClass |
|---|
| 100 | self._glyphPointClass = glyphPointClass |
|---|
| 101 | self._glyphComponentClass = glyphComponentClass |
|---|
| 102 | self._glyphAnchorClass = glyphAnchorClass |
|---|
| 103 | self._glyphImageClass = glyphImageClass |
|---|
| 104 | self._kerningClass = kerningClass |
|---|
| 105 | self._infoClass = infoClass |
|---|
| 106 | self._groupsClass = groupsClass |
|---|
| 107 | self._featuresClass = featuresClass |
|---|
| 108 | self._libClass = libClass |
|---|
| 109 | self._guidelineClass = guidelineClass |
|---|
| 110 | self._imageSetClass = imageSetClass |
|---|
| 111 | self._dataSetClass = dataSetClass |
|---|
| 112 | |
|---|
| 113 | self._path = path |
|---|
| 114 | self._ufoFormatVersion = None |
|---|
| 115 | |
|---|
| 116 | self._kerning = None |
|---|
| 117 | self._info = None |
|---|
| 118 | self._groups = None |
|---|
| 119 | self._features = None |
|---|
| 120 | self._lib = None |
|---|
| 121 | |
|---|
| 122 | self._layers = self.instantiateLayerSet() |
|---|
| 123 | self.beginSelfLayerSetNotificationObservation() |
|---|
| 124 | self._images = self.instantiateImageSet() |
|---|
| 125 | self.beginSelfImageSetNotificationObservation() |
|---|
| 126 | self._data = self.instantiateDataSet() |
|---|
| 127 | self.beginSelfDataSetNotificationObservation() |
|---|
| 128 | |
|---|
| 129 | self._dirty = False |
|---|
| 130 | |
|---|
| 131 | if path: |
|---|
| 132 | reader = UFOReader(self._path) |
|---|
| 133 | self._ufoFormatVersion = reader.formatVersion |
|---|
| 134 | # go ahead and load the layers |
|---|
| 135 | layerNames = reader.getLayerNames() |
|---|
| 136 | for layerName in layerNames: |
|---|
| 137 | glyphSet = reader.getGlyphSet(layerName) |
|---|
| 138 | layer = self._layers.newLayer(layerName, glyphSet=glyphSet) |
|---|
| 139 | layer.dirty = False |
|---|
| 140 | defaultLayerName = reader.getDefaultLayerName() |
|---|
| 141 | self._layers.layerOrder = layerNames |
|---|
| 142 | self._layers.defaultLayer = self._layers[defaultLayerName] |
|---|
| 143 | self._layers.dirty = False |
|---|
| 144 | # get the image file names |
|---|
| 145 | self._images.fileNames = reader.getImageDirectoryListing() |
|---|
| 146 | # get the data directory listing |
|---|
| 147 | self._data.fileNames = reader.getDataDirectoryListing() |
|---|
| 148 | # if the UFO version is 1, do some conversion. |
|---|
| 149 | if self._ufoFormatVersion == 1: |
|---|
| 150 | self._convertFromFormatVersion1RoboFabData() |
|---|
| 151 | # if the ufo version is < 3, read the kerning and groups |
|---|
| 152 | # right now. do this by creating a reference to the reader. |
|---|
| 153 | # otherwsie a situation could arise where the groups |
|---|
| 154 | # are modified by an external source before being read. |
|---|
| 155 | # that could create a data corruption within this object. |
|---|
| 156 | if self._ufoFormatVersion < 3: |
|---|
| 157 | self._reader = reader |
|---|
| 158 | k = self.kerning |
|---|
| 159 | g = self.groups |
|---|
| 160 | |
|---|
| 161 | if self._layers.defaultLayer is None: |
|---|
| 162 | layer = self.newLayer("public.default") |
|---|
| 163 | self._layers.defaultLayer = layer |
|---|
| 164 | |
|---|
| 165 | def _get_dispatcher(self): |
|---|
| 166 | return self._dispatcher |
|---|
| 167 | |
|---|
| 168 | dispatcher = property(_get_dispatcher, doc="The :class:`defcon.tools.notifications.NotificationCenter` assigned to this font.") |
|---|
| 169 | |
|---|
| 170 | # ------ |
|---|
| 171 | # Glyphs |
|---|
| 172 | # ------ |
|---|
| 173 | |
|---|
| 174 | def _get_glyphSet(self): |
|---|
| 175 | return self._layers.defaultLayer |
|---|
| 176 | |
|---|
| 177 | _glyphSet = property(_get_glyphSet, doc="Convenience for getting the main layer.") |
|---|
| 178 | |
|---|
| 179 | def newGlyph(self, name): |
|---|
| 180 | """ |
|---|
| 181 | Create a new glyph with **name** in the font's main layer. |
|---|
| 182 | If a glyph with that name already exists, the existing |
|---|
| 183 | glyph will be replaced with the new glyph. |
|---|
| 184 | """ |
|---|
| 185 | return self._glyphSet.newGlyph(name) |
|---|
| 186 | |
|---|
| 187 | def insertGlyph(self, glyph, name=None): |
|---|
| 188 | """ |
|---|
| 189 | Insert **glyph** into the font's main layer. |
|---|
| 190 | Optionally, the glyph can be renamed at the same time by |
|---|
| 191 | providing **name**. If a glyph with the glyph name, or |
|---|
| 192 | the name provided as **name**, already exists, the existing |
|---|
| 193 | glyph will be replaced with the new glyph. |
|---|
| 194 | """ |
|---|
| 195 | return self._glyphSet.insertGlyph(glyph, name=name) |
|---|
| 196 | |
|---|
| 197 | def __iter__(self): |
|---|
| 198 | names = self._glyphSet.keys() |
|---|
| 199 | while names: |
|---|
| 200 | name = names[0] |
|---|
| 201 | yield self._glyphSet[name] |
|---|
| 202 | names = names[1:] |
|---|
| 203 | |
|---|
| 204 | def __getitem__(self, name): |
|---|
| 205 | return self._glyphSet[name] |
|---|
| 206 | |
|---|
| 207 | def __delitem__(self, name): |
|---|
| 208 | del self._glyphSet[name] |
|---|
| 209 | |
|---|
| 210 | def __len__(self): |
|---|
| 211 | return len(self._glyphSet) |
|---|
| 212 | |
|---|
| 213 | def __contains__(self, name): |
|---|
| 214 | return name in self._glyphSet |
|---|
| 215 | |
|---|
| 216 | def keys(self): |
|---|
| 217 | return self._glyphSet.keys() |
|---|
| 218 | |
|---|
| 219 | # ------ |
|---|
| 220 | # Layers |
|---|
| 221 | # ------ |
|---|
| 222 | |
|---|
| 223 | def newLayer(self, name): |
|---|
| 224 | return self._layers.newLayer(name) |
|---|
| 225 | |
|---|
| 226 | # ---------- |
|---|
| 227 | # Attributes |
|---|
| 228 | # ---------- |
|---|
| 229 | |
|---|
| 230 | def _get_path(self): |
|---|
| 231 | return self._path |
|---|
| 232 | |
|---|
| 233 | def _set_path(self, path): |
|---|
| 234 | # XXX: this needs to be reworked for layers |
|---|
| 235 | # the file must already exist |
|---|
| 236 | assert os.path.exists(path) |
|---|
| 237 | # the glyphs directory must already exist |
|---|
| 238 | glyphsDir = os.path.join(path, "glyphs") |
|---|
| 239 | assert os.path.exists(glyphsDir) |
|---|
| 240 | # set the internal reference |
|---|
| 241 | self._path = path |
|---|
| 242 | # set the glyph set reference |
|---|
| 243 | if self._glyphSet is not None: |
|---|
| 244 | self._glyphSet.dirName = glyphsDir |
|---|
| 245 | |
|---|
| 246 | path = property(_get_path, _set_path, doc="The location of the file on disk. Setting the path should only be done when the user has moved the file in the OS interface. Setting the path is not the same as a save operation.") |
|---|
| 247 | |
|---|
| 248 | def _get_ufoFormatVersion(self): |
|---|
| 249 | return self._ufoFormatVersion |
|---|
| 250 | |
|---|
| 251 | ufoFormatVersion = property(_get_ufoFormatVersion, doc="The UFO format version that will be used when saving. This is taken from a loaded UFO during __init__. If this font was not loaded from a UFO, this will return None until the font has been saved.") |
|---|
| 252 | |
|---|
| 253 | def _get_glyphsWithOutlines(self): |
|---|
| 254 | return self._glyphSet.glyphsWithOutlines |
|---|
| 255 | |
|---|
| 256 | glyphsWithOutlines = property(_get_glyphsWithOutlines, doc="A list of glyphs containing outlines in the font's main layer.") |
|---|
| 257 | |
|---|
| 258 | def _get_componentReferences(self): |
|---|
| 259 | return self._glyphSet.componentReferences |
|---|
| 260 | |
|---|
| 261 | componentReferences = property(_get_componentReferences, doc="A dict of describing the component relationships in the font's main layer. The dictionary is of form ``{base glyph : [references]}``.") |
|---|
| 262 | |
|---|
| 263 | def _get_bounds(self): |
|---|
| 264 | return self._glyphSet.bounds |
|---|
| 265 | |
|---|
| 266 | bounds = property(_get_bounds, doc="The bounds of all glyphs in the font's main layer. This can be an expensive operation.") |
|---|
| 267 | |
|---|
| 268 | def _get_controlPointBounds(self): |
|---|
| 269 | return self._glyphSet.controlPointBounds |
|---|
| 270 | |
|---|
| 271 | controlPointBounds = property(_get_controlPointBounds, doc="The control bounds of all glyphs in the font's main layer. This only measures the point positions, it does not measure curves. So, curves without points at the extrema will not be properly measured. This is an expensive operation.") |
|---|
| 272 | |
|---|
| 273 | # ----------- |
|---|
| 274 | # Sub-Objects |
|---|
| 275 | # ----------- |
|---|
| 276 | |
|---|
| 277 | # layers |
|---|
| 278 | |
|---|
| 279 | def instantiateLayerSet(self): |
|---|
| 280 | layers = self._layerSetClass( |
|---|
| 281 | font=self, |
|---|
| 282 | libClass=self._libClass, |
|---|
| 283 | unicodeDataClass=self._unicodeDataClass, |
|---|
| 284 | guidelineClass=self._guidelineClass, |
|---|
| 285 | layerClass=self._layerClass, |
|---|
| 286 | glyphClass=self._glyphClass, |
|---|
| 287 | glyphContourClass=self._glyphContourClass, |
|---|
| 288 | glyphPointClass=self._glyphPointClass, |
|---|
| 289 | glyphComponentClass=self._glyphComponentClass, |
|---|
| 290 | glyphAnchorClass=self._glyphAnchorClass, |
|---|
| 291 | glyphImageClass=self._glyphImageClass |
|---|
| 292 | ) |
|---|
| 293 | return layers |
|---|
| 294 | |
|---|
| 295 | def beginSelfLayerSetNotificationObservation(self): |
|---|
| 296 | layers = self.layers |
|---|
| 297 | layers.addObserver(observer=self, methodName="_objectDirtyStateChange", notification="LayerSet.Changed") |
|---|
| 298 | layers.addObserver(observer=self, methodName="_layerAddedNotificationCallback", notification="LayerSet.LayerAdded") |
|---|
| 299 | layers.addObserver(observer=self, methodName="_layerWillBeDeletedNotificationCallback", notification="LayerSet.LayerWillBeDeleted") |
|---|
| 300 | |
|---|
| 301 | def endSelfLayerSetNotificationObservation(self): |
|---|
| 302 | layers = self.layers |
|---|
| 303 | if layers.dispatcher is None: |
|---|
| 304 | return |
|---|
| 305 | layers.removeObserver(observer=self, notification="LayerSet.Changed") |
|---|
| 306 | layers.removeObserver(observer=self, notification="LayerSet.LayerAdded") |
|---|
| 307 | layers.removeObserver(observer=self, notification="LayerSet.LayerWillBeDeleted") |
|---|
| 308 | layers.endSelfNotificationObservation() |
|---|
| 309 | |
|---|
| 310 | def _get_layers(self): |
|---|
| 311 | return self._layers |
|---|
| 312 | |
|---|
| 313 | layers = property(_get_layers, doc="The font's :class:`LayerSet` object.") |
|---|
| 314 | |
|---|
| 315 | # info |
|---|
| 316 | |
|---|
| 317 | def instantiateInfo(self): |
|---|
| 318 | info = self._infoClass( |
|---|
| 319 | font=self, |
|---|
| 320 | guidelineClass=self._guidelineClass |
|---|
| 321 | ) |
|---|
| 322 | return info |
|---|
| 323 | |
|---|
| 324 | def beginSelfInfoSetNotificationObservation(self): |
|---|
| 325 | info = self.info |
|---|
| 326 | info.addObserver(observer=self, methodName="_objectDirtyStateChange", notification="Info.Changed") |
|---|
| 327 | |
|---|
| 328 | def endSelfInfoSetNotificationObservation(self): |
|---|
| 329 | if self._info is None: |
|---|
| 330 | return |
|---|
| 331 | if self._info.dispatcher is None: |
|---|
| 332 | return |
|---|
| 333 | self._info.removeObserver(observer=self, notification="Info.Changed") |
|---|
| 334 | self._info.endSelfNotificationObservation() |
|---|
| 335 | |
|---|
| 336 | def _get_info(self): |
|---|
| 337 | if self._info is None: |
|---|
| 338 | self._info = self.instantiateInfo() |
|---|
| 339 | self.beginSelfInfoSetNotificationObservation() |
|---|
| 340 | reader = None |
|---|
| 341 | if self._path is not None: |
|---|
| 342 | self._info.disableNotifications() |
|---|
| 343 | reader = UFOReader(self._path) |
|---|
| 344 | reader.readInfo(self._info) |
|---|
| 345 | self._info.dirty = False |
|---|
| 346 | self._info.enableNotifications() |
|---|
| 347 | self._stampInfoDataState(reader) |
|---|
| 348 | return self._info |
|---|
| 349 | |
|---|
| 350 | info = property(_get_info, doc="The font's :class:`Info` object.") |
|---|
| 351 | |
|---|
| 352 | # kerning |
|---|
| 353 | |
|---|
| 354 | def _loadKerningAndGroups(self): |
|---|
| 355 | # read |
|---|
| 356 | if hasattr(self, "_reader"): |
|---|
| 357 | reader = self._reader |
|---|
| 358 | else: |
|---|
| 359 | reader = UFOReader(self._path) |
|---|
| 360 | kerning = reader.readKerning() |
|---|
| 361 | groups = reader.readGroups() |
|---|
| 362 | # validate |
|---|
| 363 | if not kerningValidator(kerning, groups): |
|---|
| 364 | raise DefconError("The kerning data is not valid.") |
|---|
| 365 | # store kerning |
|---|
| 366 | self._kerning = self.instantiateKerning() |
|---|
| 367 | self.beginSelfKerningNotificationObservation() |
|---|
| 368 | self._kerning.disableNotifications() |
|---|
| 369 | self._kerning.update(kerning) |
|---|
| 370 | self._kerning.dirty = False |
|---|
| 371 | self._kerning.enableNotifications() |
|---|
| 372 | self._stampKerningDataState(reader) |
|---|
| 373 | # store groups |
|---|
| 374 | self._groups = self.instantiateGroups() |
|---|
| 375 | self.beginSelfGroupsNotificationObservation() |
|---|
| 376 | self._groups.disableNotifications() |
|---|
| 377 | self._groups.update(groups) |
|---|
| 378 | self._groups.dirty = False |
|---|
| 379 | self._groups.enableNotifications() |
|---|
| 380 | self._stampGroupsDataState(reader) |
|---|
| 381 | |
|---|
| 382 | def instantiateKerning(self): |
|---|
| 383 | kerning = self._kerningClass( |
|---|
| 384 | font=self |
|---|
| 385 | ) |
|---|
| 386 | return kerning |
|---|
| 387 | |
|---|
| 388 | def beginSelfKerningNotificationObservation(self): |
|---|
| 389 | kerning = self.kerning |
|---|
| 390 | kerning.addObserver(observer=self, methodName="_objectDirtyStateChange", notification="Kerning.Changed") |
|---|
| 391 | |
|---|
| 392 | def endSelfKerningNotificationObservation(self): |
|---|
| 393 | if self._kerning is None: |
|---|
| 394 | return |
|---|
| 395 | if self._kerning.dispatcher is None: |
|---|
| 396 | return |
|---|
| 397 | self._kerning.removeObserver(observer=self, notification="Kerning.Changed") |
|---|
| 398 | self._kerning.endSelfNotificationObservation() |
|---|
| 399 | |
|---|
| 400 | def _get_kerning(self): |
|---|
| 401 | if self._kerning is None: |
|---|
| 402 | if self._path is None: |
|---|
| 403 | self._kerning = self.instantiateKerning() |
|---|
| 404 | self.beginSelfKerningNotificationObservation() |
|---|
| 405 | self._stampKerningDataState(None) |
|---|
| 406 | else: |
|---|
| 407 | self._loadKerningAndGroups() |
|---|
| 408 | return self._kerning |
|---|
| 409 | |
|---|
| 410 | kerning = property(_get_kerning, doc="The font's :class:`Kerning` object.") |
|---|
| 411 | |
|---|
| 412 | # groups |
|---|
| 413 | |
|---|
| 414 | def instantiateGroups(self): |
|---|
| 415 | groups = self._groupsClass( |
|---|
| 416 | font=self |
|---|
| 417 | ) |
|---|
| 418 | return groups |
|---|
| 419 | |
|---|
| 420 | def beginSelfGroupsNotificationObservation(self): |
|---|
| 421 | groups = self.groups |
|---|
| 422 | groups.addObserver(observer=self, methodName="_objectDirtyStateChange", notification="Groups.Changed") |
|---|
| 423 | |
|---|
| 424 | def endSelfGroupsNotificationObservation(self): |
|---|
| 425 | if self._groups is None: |
|---|
| 426 | return |
|---|
| 427 | if self._groups.dispatcher is None: |
|---|
| 428 | return |
|---|
| 429 | self._groups.removeObserver(observer=self, notification="Groups.Changed") |
|---|
| 430 | self._groups.endSelfNotificationObservation() |
|---|
| 431 | |
|---|
| 432 | def _get_groups(self): |
|---|
| 433 | if self._groups is None: |
|---|
| 434 | if self._path is None: |
|---|
| 435 | self._groups = self.instantiateGroups() |
|---|
| 436 | self.beginSelfGroupsNotificationObservation() |
|---|
| 437 | self._stampGroupsDataState(None) |
|---|
| 438 | else: |
|---|
| 439 | self._loadKerningAndGroups() |
|---|
| 440 | return self._groups |
|---|
| 441 | |
|---|
| 442 | groups = property(_get_groups, doc="The font's :class:`Groups` object.") |
|---|
| 443 | |
|---|
| 444 | # features |
|---|
| 445 | |
|---|
| 446 | def instantiateFeatures(self): |
|---|
| 447 | features = self._featuresClass( |
|---|
| 448 | font=self |
|---|
| 449 | ) |
|---|
| 450 | return features |
|---|
| 451 | |
|---|
| 452 | def beginSelfFeaturesNotificationObservation(self): |
|---|
| 453 | features = self.features |
|---|
| 454 | features.addObserver(observer=self, methodName="_objectDirtyStateChange", notification="Features.Changed") |
|---|
| 455 | |
|---|
| 456 | def endSelfFeaturesNotificationObservation(self): |
|---|
| 457 | if self._features is None: |
|---|
| 458 | return |
|---|
| 459 | if self._features.dispatcher is None: |
|---|
| 460 | return |
|---|
| 461 | self._features.removeObserver(observer=self, notification="Features.Changed") |
|---|
| 462 | self._features.endSelfNotificationObservation() |
|---|
| 463 | |
|---|
| 464 | def _get_features(self): |
|---|
| 465 | if self._features is None: |
|---|
| 466 | self._features = self.instantiateFeatures() |
|---|
| 467 | self.beginSelfFeaturesNotificationObservation() |
|---|
| 468 | reader = None |
|---|
| 469 | if self._path is not None: |
|---|
| 470 | self._features.disableNotifications() |
|---|
| 471 | reader = UFOReader(self._path) |
|---|
| 472 | t = reader.readFeatures() |
|---|
| 473 | self._features.text = t |
|---|
| 474 | self._features.dirty = False |
|---|
| 475 | self._features.enableNotifications() |
|---|
| 476 | self._stampFeaturesDataState(reader) |
|---|
| 477 | return self._features |
|---|
| 478 | |
|---|
| 479 | features = property(_get_features, doc="The font's :class:`Features` object.") |
|---|
| 480 | |
|---|
| 481 | # lib |
|---|
| 482 | |
|---|
| 483 | def instantiateLib(self): |
|---|
| 484 | lib = self._libClass( |
|---|
| 485 | font=self |
|---|
| 486 | ) |
|---|
| 487 | return lib |
|---|
| 488 | |
|---|
| 489 | def beginSelfLibNotificationObservation(self): |
|---|
| 490 | self._lib.addObserver(observer=self, methodName="_objectDirtyStateChange", notification="Lib.Changed") |
|---|
| 491 | |
|---|
| 492 | def endSelfLibNotificationObservation(self): |
|---|
| 493 | if self._lib is None: |
|---|
| 494 | return |
|---|
| 495 | if self._lib.dispatcher is None: |
|---|
| 496 | return |
|---|
| 497 | self._lib.removeObserver(observer=self, notification="Lib.Changed") |
|---|
| 498 | self._lib.endSelfNotificationObservation() |
|---|
| 499 | |
|---|
| 500 | def _get_lib(self): |
|---|
| 501 | if self._lib is None: |
|---|
| 502 | self._lib = self.instantiateLib() |
|---|
| 503 | self.beginSelfLibNotificationObservation() |
|---|
| 504 | reader = None |
|---|
| 505 | if self._path is not None: |
|---|
| 506 | self._lib.disableNotifications() |
|---|
| 507 | reader = UFOReader(self._path) |
|---|
| 508 | d = reader.readLib() |
|---|
| 509 | self._lib.update(d) |
|---|
| 510 | self._lib.enableNotifications() |
|---|
| 511 | self._stampLibDataState(reader) |
|---|
| 512 | return self._lib |
|---|
| 513 | |
|---|
| 514 | lib = property(_get_lib, doc="The font's :class:`Lib` object.") |
|---|
| 515 | |
|---|
| 516 | # images |
|---|
| 517 | |
|---|
| 518 | def instantiateImageSet(self): |
|---|
| 519 | imageSet = self._imageSetClass( |
|---|
| 520 | font=self |
|---|
| 521 | ) |
|---|
| 522 | return imageSet |
|---|
| 523 | |
|---|
| 524 | def beginSelfImageSetNotificationObservation(self): |
|---|
| 525 | self._images.addObserver(observer=self, methodName="_objectDirtyStateChange", notification="ImageSet.Changed") |
|---|
| 526 | |
|---|
| 527 | def endSelfImageSetNotificationObservation(self): |
|---|
| 528 | if self._images.dispatcher is None: |
|---|
| 529 | return |
|---|
| 530 | self._images.removeObserver(observer=self, notification="ImageSet.Changed") |
|---|
| 531 | self._images.endSelfNotificationObservation() |
|---|
| 532 | |
|---|
| 533 | def _get_images(self): |
|---|
| 534 | return self._images |
|---|
| 535 | |
|---|
| 536 | images = property(_get_images, doc="The font's :class:`ImageSet` object.") |
|---|
| 537 | |
|---|
| 538 | # data |
|---|
| 539 | |
|---|
| 540 | def instantiateDataSet(self): |
|---|
| 541 | dataSet = self._dataSetClass( |
|---|
| 542 | font=self |
|---|
| 543 | ) |
|---|
| 544 | return dataSet |
|---|
| 545 | |
|---|
| 546 | def beginSelfDataSetNotificationObservation(self): |
|---|
| 547 | self._data.addObserver(observer=self, methodName="_objectDirtyStateChange", notification="DataSet.Changed") |
|---|
| 548 | |
|---|
| 549 | def endSelfDataSetNotificationObservation(self): |
|---|
| 550 | if self._data.dispatcher is None: |
|---|
| 551 | return |
|---|
| 552 | self._data.removeObserver(observer=self, notification="DataSet.Changed") |
|---|
| 553 | self._data.endSelfNotificationObservation() |
|---|
| 554 | |
|---|
| 555 | def _get_data(self): |
|---|
| 556 | return self._data |
|---|
| 557 | |
|---|
| 558 | data = property(_get_data, doc="The font's :class:`DataSet` object.") |
|---|
| 559 | |
|---|
| 560 | # unicode data (legacy) |
|---|
| 561 | |
|---|
| 562 | def _get_unicodeData(self): |
|---|
| 563 | return self._glyphSet.unicodeData |
|---|
| 564 | |
|---|
| 565 | unicodeData = property(_get_unicodeData, doc="The font's :class:`UnicodeData` object.") |
|---|
| 566 | |
|---|
| 567 | # glyph order |
|---|
| 568 | |
|---|
| 569 | def _get_glyphOrder(self): |
|---|
| 570 | return list(self.lib.get("public.glyphOrder", [])) |
|---|
| 571 | |
|---|
| 572 | def _set_glyphOrder(self, value): |
|---|
| 573 | oldValue = self.lib.get("public.glyphOrder") |
|---|
| 574 | if oldValue == value: |
|---|
| 575 | return |
|---|
| 576 | if value is None or len(value) == 0: |
|---|
| 577 | value = None |
|---|
| 578 | if "public.glyphOrder" in self.lib: |
|---|
| 579 | del self.lib["public.glyphOrder"] |
|---|
| 580 | else: |
|---|
| 581 | self.lib["public.glyphOrder"] = value |
|---|
| 582 | self.postNotification("Font.GlyphOrderChanged", data=dict(oldValue=oldValue, newValue=value)) |
|---|
| 583 | |
|---|
| 584 | glyphOrder = property(_get_glyphOrder, _set_glyphOrder, doc="The font's glyph order. When setting the value must be a list of glyph names. There is no requirement, nor guarantee, that the list will contain only names of glyphs in the font. Setting this posts *Font.GlyphOrderChanged* and *Font.Changed* notifications.") |
|---|
| 585 | |
|---|
| 586 | def updateGlyphOrder(self, addedGlyph=None, removedGlyph=None): |
|---|
| 587 | """ |
|---|
| 588 | This method tries to keep the glyph order in sync. |
|---|
| 589 | This should not be called externally. It may be overriden |
|---|
| 590 | by subclasses as needed. |
|---|
| 591 | """ |
|---|
| 592 | order = self.glyphOrder |
|---|
| 593 | if addedGlyph is not None: |
|---|
| 594 | if addedGlyph not in order: |
|---|
| 595 | order.append(addedGlyph) |
|---|
| 596 | elif removedGlyph is not None: |
|---|
| 597 | if removedGlyph in order: |
|---|
| 598 | count = order.count(removedGlyph) |
|---|
| 599 | if count == 1: |
|---|
| 600 | order.remove(removedGlyph) |
|---|
| 601 | else: |
|---|
| 602 | for i in range(count): |
|---|
| 603 | order.remove(removedGlyph) |
|---|
| 604 | self.glyphOrder = order |
|---|
| 605 | |
|---|
| 606 | # ------- |
|---|
| 607 | # Methods |
|---|
| 608 | # ------- |
|---|
| 609 | |
|---|
| 610 | def getSaveProgressBarTickCount(self, formatVersion=None): |
|---|
| 611 | """ |
|---|
| 612 | Get the number of ticks that will be used by a progress bar |
|---|
| 613 | in the save method. Subclasses may override this method to |
|---|
| 614 | implement custom saving behavior. |
|---|
| 615 | """ |
|---|
| 616 | # if not format version is given, use the existing. |
|---|
| 617 | # if that doesn't exist, go to 3. |
|---|
| 618 | if formatVersion is None: |
|---|
| 619 | formatVersion = self._ufoFormatVersion |
|---|
| 620 | if formatVersion is None: |
|---|
| 621 | formatVersion = 3 |
|---|
| 622 | count = 0 |
|---|
| 623 | count += 1 # info |
|---|
| 624 | count += 1 # groups |
|---|
| 625 | count += 1 # lib |
|---|
| 626 | if formatVersion != self._ufoFormatVersion and formatVersion < 3: |
|---|
| 627 | count += 1 |
|---|
| 628 | else: |
|---|
| 629 | count += int(self.kerning.dirty) |
|---|
| 630 | if formatVersion >= 2: |
|---|
| 631 | count += int(self.features.dirty) |
|---|
| 632 | if formatVersion >= 3: |
|---|
| 633 | count += self.images.getSaveProgressBarTickCount(formatVersion) |
|---|
| 634 | count += self.data.getSaveProgressBarTickCount(formatVersion) |
|---|
| 635 | count += self.layers.getSaveProgressBarTickCount(formatVersion) |
|---|
| 636 | return count |
|---|
| 637 | |
|---|
| 638 | def save(self, path=None, formatVersion=None, removeUnreferencedImages=False, progressBar=None): |
|---|
| 639 | """ |
|---|
| 640 | Save the font to **path**. If path is None, the path |
|---|
| 641 | from the last save or when the font was first opened |
|---|
| 642 | will be used. |
|---|
| 643 | |
|---|
| 644 | The UFO will be saved using the format found at ``ufoFormatVersion``. |
|---|
| 645 | This value is either the format version from the exising UFO or |
|---|
| 646 | the format version specified in a previous save. If neither of |
|---|
| 647 | these is available, the UFO will be written as format version 2. |
|---|
| 648 | If you wish to specifiy the format version for saving, pass |
|---|
| 649 | the desired number as the **formatVersion** argument. |
|---|
| 650 | |
|---|
| 651 | Optionally, the UFO can be purged of unreferenced images |
|---|
| 652 | during this operation. To do this, pass ``True`` as the |
|---|
| 653 | value for the removeUnreferencedImages argument. |
|---|
| 654 | """ |
|---|
| 655 | saveAs = False |
|---|
| 656 | if path is not None and path != self._path: |
|---|
| 657 | saveAs = True |
|---|
| 658 | else: |
|---|
| 659 | path = self._path |
|---|
| 660 | # sanity checks on layer data before doing anything destructive |
|---|
| 661 | assert self.layers.defaultLayer is not None |
|---|
| 662 | if self.layers.defaultLayer.name != "public.default": |
|---|
| 663 | assert "public.default" not in self.layers.layerOrder |
|---|
| 664 | # validate kerning and groups before doing anything destructive |
|---|
| 665 | if self._kerning is not None and self._groups is not None: |
|---|
| 666 | if not kerningValidator(self._kerning, self._groups): |
|---|
| 667 | raise DefconError("The kerning data is not valid.") |
|---|
| 668 | ## work out the format version |
|---|
| 669 | # if None is given, fallback to the one that |
|---|
| 670 | # came in when the UFO was loaded |
|---|
| 671 | if formatVersion is None and self._ufoFormatVersion is not None: |
|---|
| 672 | formatVersion = self._ufoFormatVersion |
|---|
| 673 | # otherwise fallback to 3 |
|---|
| 674 | elif self._ufoFormatVersion is None: |
|---|
| 675 | formatVersion = 3 |
|---|
| 676 | # if down-converting, use a temp directory |
|---|
| 677 | downConvertinginPlace = False |
|---|
| 678 | if path == self._path and formatVersion < self._ufoFormatVersion: |
|---|
| 679 | downConvertinginPlace = True |
|---|
| 680 | path = os.path.join(tempfile.mkdtemp(), "temp.ufo") |
|---|
| 681 | try: |
|---|
| 682 | # make a UFOWriter |
|---|
| 683 | writer = UFOWriter(path, formatVersion=formatVersion) |
|---|
| 684 | # if changing ufo format versions, flag all objects |
|---|
| 685 | # as dirty so that they will be saved |
|---|
| 686 | if self._ufoFormatVersion != formatVersion: |
|---|
| 687 | self.info.dirty = True |
|---|
| 688 | self.groups.dirty = True |
|---|
| 689 | self.kerning.dirty = True |
|---|
| 690 | self.lib.dirty = True |
|---|
| 691 | if formatVersion > 1: |
|---|
| 692 | self.features.dirty = True |
|---|
| 693 | # save the objects |
|---|
| 694 | self.saveInfo(writer=writer, saveAs=saveAs, progressBar=progressBar) |
|---|
| 695 | self.saveGroups(writer=writer, saveAs=saveAs, progressBar=progressBar) |
|---|
| 696 | self.saveKerning(writer=writer, saveAs=saveAs, progressBar=progressBar) |
|---|
| 697 | self.saveLib(writer=writer, saveAs=saveAs, progressBar=progressBar) |
|---|
| 698 | if formatVersion >= 2: |
|---|
| 699 | self.saveFeatures(writer=writer, saveAs=saveAs, progressBar=progressBar) |
|---|
| 700 | if formatVersion >= 3: |
|---|
| 701 | self.saveImages(writer=writer, removeUnreferencedImages=removeUnreferencedImages, saveAs=saveAs, progressBar=progressBar) |
|---|
| 702 | self.saveData(writer=writer, saveAs=saveAs, progressBar=progressBar) |
|---|
| 703 | self.layers.save(writer, saveAs=saveAs, progressBar=progressBar) |
|---|
| 704 | writer.setModificationTime() |
|---|
| 705 | if downConvertinginPlace: |
|---|
| 706 | shutil.rmtree(self._path) |
|---|
| 707 | shutil.move(path, self._path) |
|---|
| 708 | finally: |
|---|
| 709 | # if down converting in place, handle the temp |
|---|
| 710 | if downConvertinginPlace: |
|---|
| 711 | shutil.rmtree(os.path.dirname(path)) |
|---|
| 712 | path = self._path |
|---|
| 713 | # done |
|---|
| 714 | self._path = path |
|---|
| 715 | self._ufoFormatVersion = formatVersion |
|---|
| 716 | self.dirty = False |
|---|
| 717 | |
|---|
| 718 | def saveInfo(self, writer, saveAs=False, progressBar=None): |
|---|
| 719 | """ |
|---|
| 720 | Save info. This method should not be called externally. |
|---|
| 721 | Subclasses may override this method to implement custom saving behavior. |
|---|
| 722 | """ |
|---|
| 723 | # info should always be saved |
|---|
| 724 | if progressBar is not None: |
|---|
| 725 | progressBar.update(text="Saving info...", increment=0) |
|---|
| 726 | writer.writeInfo(self.info) |
|---|
| 727 | self.info.dirty = False |
|---|
| 728 | self._stampInfoDataState(UFOReader(writer.path)) |
|---|
| 729 | if progressBar is not None: |
|---|
| 730 | progressBar.update() |
|---|
| 731 | |
|---|
| 732 | def saveGroups(self, writer, saveAs=False, progressBar=None): |
|---|
| 733 | """ |
|---|
| 734 | Save groups. This method should not be called externally. |
|---|
| 735 | Subclasses may override this method to implement custom saving behavior. |
|---|
| 736 | """ |
|---|
| 737 | # groups should always be saved |
|---|
| 738 | if progressBar is not None: |
|---|
| 739 | progressBar.update(text="Saving groups...", increment=0) |
|---|
| 740 | writer.writeGroups(self.groups) |
|---|
| 741 | self.groups.dirty = False |
|---|
| 742 | self._stampGroupsDataState(UFOReader(writer.path)) |
|---|
| 743 | if progressBar is not None: |
|---|
| 744 | progressBar.update() |
|---|
| 745 | |
|---|
| 746 | def saveKerning(self, writer, saveAs=False, progressBar=None): |
|---|
| 747 | """ |
|---|
| 748 | Save kerning. This method should not be called externally. |
|---|
| 749 | Subclasses may override this method to implement custom saving behavior. |
|---|
| 750 | """ |
|---|
| 751 | if progressBar is not None: |
|---|
| 752 | progressBar.update(text="Saving kerning...", increment=0) |
|---|
| 753 | if self.kerning.dirty or saveAs: |
|---|
| 754 | writer.writeKerning(self.kerning) |
|---|
| 755 | self.kerning.dirty = False |
|---|
| 756 | self._stampKerningDataState(UFOReader(writer.path)) |
|---|
| 757 | if progressBar is not None: |
|---|
| 758 | progressBar.update() |
|---|
| 759 | |
|---|
| 760 | def saveFeatures(self, writer, saveAs=False, progressBar=None): |
|---|
| 761 | """ |
|---|
| 762 | Save features. This method should not be called externally. |
|---|
| 763 | Subclasses may override this method to implement custom saving behavior. |
|---|
| 764 | """ |
|---|
| 765 | if progressBar is not None: |
|---|
| 766 | progressBar.update(text="Saving features...", increment=0) |
|---|
| 767 | if self.features.dirty or saveAs: |
|---|
| 768 | writer.writeFeatures(self.features.text) |
|---|
| 769 | self.features.dirty = False |
|---|
| 770 | self._stampFeaturesDataState(UFOReader(writer.path)) |
|---|
| 771 | if progressBar is not None: |
|---|
| 772 | progressBar.update() |
|---|
| 773 | |
|---|
| 774 | def saveLib(self, writer, saveAs=False, progressBar=None): |
|---|
| 775 | """ |
|---|
| 776 | Save lib. This method should not be called externally. |
|---|
| 777 | Subclasses may override this method to implement custom saving behavior. |
|---|
| 778 | """ |
|---|
| 779 | # lib should always be saved |
|---|
| 780 | if progressBar is not None: |
|---|
| 781 | progressBar.update(text="Saving lib...", increment=0) |
|---|
| 782 | # if making format version 1, do some |
|---|
| 783 | # temporary down conversion before |
|---|
| 784 | # passing the lib to the writer |
|---|
| 785 | libCopy = dict(self.lib) |
|---|
| 786 | if writer.formatVersion == 1: |
|---|
| 787 | self._convertToFormatVersion1RoboFabData(libCopy) |
|---|
| 788 | writer.writeLib(libCopy) |
|---|
| 789 | self.lib.dirty = False |
|---|
| 790 | self._stampLibDataState(UFOReader(writer.path)) |
|---|
| 791 | if progressBar is not None: |
|---|
| 792 | progressBar.update() |
|---|
| 793 | |
|---|
| 794 | def saveImages(self, writer, removeUnreferencedImages=False, saveAs=False, progressBar=None): |
|---|
| 795 | """ |
|---|
| 796 | Save images. This method should not be called externally. |
|---|
| 797 | Subclasses may override this method to implement custom saving behavior. |
|---|
| 798 | """ |
|---|
| 799 | if progressBar is not None: |
|---|
| 800 | progressBar.update(text="Saving images...", increment=0) |
|---|
| 801 | self.images.save(writer, removeUnreferencedImages=removeUnreferencedImages, saveAs=saveAs, progressBar=progressBar) |
|---|
| 802 | if progressBar is not None: |
|---|
| 803 | progressBar.update() |
|---|
| 804 | |
|---|
| 805 | def saveData(self, writer, saveAs=False, progressBar=None): |
|---|
| 806 | """ |
|---|
| 807 | Save data. This method should not be called externally. |
|---|
| 808 | Subclasses may override this method to implement custom saving behavior. |
|---|
| 809 | """ |
|---|
| 810 | if progressBar is not None: |
|---|
| 811 | progressBar.update(text="Saving data...", increment=0) |
|---|
| 812 | self.data.save(writer, saveAs=saveAs, progressBar=progressBar) |
|---|
| 813 | if progressBar is not None: |
|---|
| 814 | progressBar.update() |
|---|
| 815 | |
|---|
| 816 | # ------------------------ |
|---|
| 817 | # Notification Observation |
|---|
| 818 | # ------------------------ |
|---|
| 819 | |
|---|
| 820 | def endSelfNotificationObservation(self): |
|---|
| 821 | if self.dispatcher is None: |
|---|
| 822 | return |
|---|
| 823 | self.endSelfLayerSetNotificationObservation() |
|---|
| 824 | self.endSelfInfoSetNotificationObservation() |
|---|
| 825 | self.endSelfKerningNotificationObservation() |
|---|
| 826 | self.endSelfGroupsNotificationObservation() |
|---|
| 827 | self.endSelfLibNotificationObservation() |
|---|
| 828 | self.endSelfFeaturesNotificationObservation() |
|---|
| 829 | self.endSelfImageSetNotificationObservation() |
|---|
| 830 | self.endSelfDataSetNotificationObservation() |
|---|
| 831 | super(Font, self).endSelfNotificationObservation() |
|---|
| 832 | |
|---|
| 833 | def _objectDirtyStateChange(self, notification): |
|---|
| 834 | if notification.object.dirty: |
|---|
| 835 | self.dirty = True |
|---|
| 836 | |
|---|
| 837 | def _layerAddedNotificationCallback(self, notification): |
|---|
| 838 | name = notification.data["name"] |
|---|
| 839 | layer = self.layers[name] |
|---|
| 840 | layer.addObserver(self, "_glyphAddedNotificationCallback", "Layer.GlyphAdded") |
|---|
| 841 | layer.addObserver(self, "_glyphDeletedNotificationCallback", "Layer.GlyphDeleted") |
|---|
| 842 | layer.addObserver(self, "_glyphRenamedNotificationCallback", "Layer.GlyphNameChanged") |
|---|
| 843 | |
|---|
| 844 | def _layerWillBeDeletedNotificationCallback(self, notification): |
|---|
| 845 | name = notification.data["name"] |
|---|
| 846 | layer = self.layers[name] |
|---|
| 847 | layer.removeObserver(self, "Layer.GlyphAdded") |
|---|
| 848 | layer.removeObserver(self, "Layer.GlyphDeleted") |
|---|
| 849 | layer.removeObserver(self, "Layer.GlyphNameChanged") |
|---|
| 850 | |
|---|
| 851 | def _glyphAddedNotificationCallback(self, notification): |
|---|
| 852 | name = notification.data["name"] |
|---|
| 853 | self.updateGlyphOrder(addedGlyph=name) |
|---|
| 854 | |
|---|
| 855 | def _glyphDeletedNotificationCallback(self, notification): |
|---|
| 856 | name = notification.data["name"] |
|---|
| 857 | stillExists = False |
|---|
| 858 | for layer in self.layers: |
|---|
| 859 | if name in layer: |
|---|
| 860 | stillExists = True |
|---|
| 861 | break |
|---|
| 862 | if not stillExists: |
|---|
| 863 | self.updateGlyphOrder(removedGlyph=name) |
|---|
| 864 | |
|---|
| 865 | def _glyphRenamedNotificationCallback(self, notification): |
|---|
| 866 | oldName = notification.data["oldValue"] |
|---|
| 867 | newName = notification.data["newValue"] |
|---|
| 868 | oldStillExists = False |
|---|
| 869 | for layer in self.layers: |
|---|
| 870 | if oldName in layer: |
|---|
| 871 | oldStillExists = True |
|---|
| 872 | break |
|---|
| 873 | if not oldStillExists: |
|---|
| 874 | self.updateGlyphOrder(removedGlyph=oldName) |
|---|
| 875 | self.updateGlyphOrder(addedGlyph=newName) |
|---|
| 876 | |
|---|
| 877 | # --------------------- |
|---|
| 878 | # External Edit Support |
|---|
| 879 | # --------------------- |
|---|
| 880 | |
|---|
| 881 | # data stamping |
|---|
| 882 | |
|---|
| 883 | def _stampFontDataState(self, obj, fileName, reader=None): |
|---|
| 884 | # font is not on disk |
|---|
| 885 | if self.path is None: |
|---|
| 886 | return |
|---|
| 887 | # data has not been loaded |
|---|
| 888 | if obj is None: |
|---|
| 889 | return |
|---|
| 890 | # make a reader if necessary |
|---|
| 891 | if reader is None: |
|---|
| 892 | reader = UFOReader(self.path) |
|---|
| 893 | # get the mod time from the reader |
|---|
| 894 | modTime = reader.getFileModificationTime(fileName) |
|---|
| 895 | # file is not in the UFO |
|---|
| 896 | if modTime is None: |
|---|
| 897 | data = None |
|---|
| 898 | modTime = -1 |
|---|
| 899 | # get the data |
|---|
| 900 | else: |
|---|
| 901 | data = reader.readBytesFromPath(fileName) |
|---|
| 902 | # store the data |
|---|
| 903 | obj._dataOnDisk = data |
|---|
| 904 | obj._dataOnDiskTimeStamp = modTime |
|---|
| 905 | |
|---|
| 906 | def _stampInfoDataState(self, reader=None): |
|---|
| 907 | self._stampFontDataState(self._info, "fontinfo.plist", reader=reader) |
|---|
| 908 | |
|---|
| 909 | def _stampKerningDataState(self, reader=None): |
|---|
| 910 | self._stampFontDataState(self._kerning, "kerning.plist", reader=reader) |
|---|
| 911 | |
|---|
| 912 | def _stampGroupsDataState(self, reader=None): |
|---|
| 913 | self._stampFontDataState(self._groups, "groups.plist", reader=reader) |
|---|
| 914 | |
|---|
| 915 | def _stampFeaturesDataState(self, reader=None): |
|---|
| 916 | self._stampFontDataState(self._features, "features.fea", reader=reader) |
|---|
| 917 | |
|---|
| 918 | def _stampLibDataState(self, reader=None): |
|---|
| 919 | self._stampFontDataState(self._lib, "lib.plist", reader=reader) |
|---|
| 920 | |
|---|
| 921 | # data comparison |
|---|
| 922 | |
|---|
| 923 | def testForExternalChanges(self): |
|---|
| 924 | """ |
|---|
| 925 | Test the UFO for changes that occured outside of this font's |
|---|
| 926 | tree of objects. This returns a dictionary describing the changes:: |
|---|
| 927 | |
|---|
| 928 | { |
|---|
| 929 | "info" : bool, # True if changed, False if not changed |
|---|
| 930 | "kerning" : bool, # True if changed, False if not changed |
|---|
| 931 | "groups" : bool, # True if changed, False if not changed |
|---|
| 932 | "features" : bool, # True if changed, False if not changed |
|---|
| 933 | "lib" : bool, # True if changed, False if not changed |
|---|
| 934 | "layers" : { |
|---|
| 935 | "defaultLayer" : bool, # True if changed, False if not changed |
|---|
| 936 | "order" : bool, # True if changed, False if not changed |
|---|
| 937 | "added" : ["layer name 1", "layer name 2"], |
|---|
| 938 | "deleted" : ["layer name 1", "layer name 2"], |
|---|
| 939 | "modified" : { |
|---|
| 940 | "info" : bool, # True if changed, False if not changed |
|---|
| 941 | "modified" : ["glyph name 1", "glyph name 2"], |
|---|
| 942 | "added" : ["glyph name 1", "glyph name 2"], |
|---|
| 943 | "deleted" : ["glyph name 1", "glyph name 2"] |
|---|
| 944 | } |
|---|
| 945 | }, |
|---|
| 946 | "images" : { |
|---|
| 947 | "modified" : ["image name 1", "image name 2"], |
|---|
| 948 | "added" : ["image name 1", "image name 2"], |
|---|
| 949 | "deleted" : ["image name 1", "image name 2"], |
|---|
| 950 | }, |
|---|
| 951 | "data" : { |
|---|
| 952 | "modified" : ["file name 1", "file name 2"], |
|---|
| 953 | "added" : ["file name 1", "file name 2"], |
|---|
| 954 | "deleted" : ["file name 1", "file name 2"], |
|---|
| 955 | } |
|---|
| 956 | } |
|---|
| 957 | |
|---|
| 958 | It is important to keep in mind that the user could have created |
|---|
| 959 | conflicting data outside of the font's tree of objects. For example, |
|---|
| 960 | say the user has set ``font.info.unitsPerEm = 1000`` inside of the |
|---|
| 961 | font's :class:`Info` object and the user has not saved this change. |
|---|
| 962 | In the the font's fontinfo.plist file, the user sets the unitsPerEm value |
|---|
| 963 | to 2000. Which value is current? Which value is right? defcon leaves |
|---|
| 964 | this decision up to you. |
|---|
| 965 | """ |
|---|
| 966 | assert self.path is not None |
|---|
| 967 | reader = UFOReader(self.path) |
|---|
| 968 | infoChanged = self._testInfoForExternalModifications(reader) |
|---|
| 969 | kerningChanged = self._testKerningForExternalModifications(reader) |
|---|
| 970 | groupsChanged = self._testGroupsForExternalModifications(reader) |
|---|
| 971 | featuresChanged = self._testFeaturesForExternalModifications(reader) |
|---|
| 972 | libChanged = self._testLibForExternalModifications(reader) |
|---|
| 973 | layerChanges = self.layers.testForExternalChanges(reader) |
|---|
| 974 | modifiedImages = addedImages = deletedImages = [] |
|---|
| 975 | if self._images is not None: |
|---|
| 976 | modifiedImages, addedImages, deletedImages = self._images.testForExternalChanges(reader) |
|---|
| 977 | modifiedData = addedData = deletedData = [] |
|---|
| 978 | if self._data is not None: |
|---|
| 979 | modifiedData, addedData, deletedData = self._data.testForExternalChanges(reader) |
|---|
| 980 | # deprecated stuff |
|---|
| 981 | defaultLayerName = self.layers.defaultLayer.name |
|---|
| 982 | modifiedGlyphs = layerChanges["modified"].get(defaultLayerName, {}).get("modified") |
|---|
| 983 | addedGlyphs = layerChanges["modified"].get(defaultLayerName, {}).get("added") |
|---|
| 984 | deletedGlyphs = layerChanges["modified"].get(defaultLayerName, {}).get("deleted") |
|---|
| 985 | return dict( |
|---|
| 986 | info=infoChanged, |
|---|
| 987 | kerning=kerningChanged, |
|---|
| 988 | groups=groupsChanged, |
|---|
| 989 | features=featuresChanged, |
|---|
| 990 | lib=libChanged, |
|---|
| 991 | layers=layerChanges, |
|---|
| 992 | images=dict( |
|---|
| 993 | modified=modifiedImages, |
|---|
| 994 | added=addedImages, |
|---|
| 995 | deleted=deletedImages |
|---|
| 996 | ), |
|---|
| 997 | data=dict( |
|---|
| 998 | modifiedData=modifiedData, |
|---|
| 999 | addedData=addedData, |
|---|
| 1000 | deletedData=deletedData |
|---|
| 1001 | ), |
|---|
| 1002 | # deprecated |
|---|
| 1003 | modifiedGlyphs=modifiedGlyphs, |
|---|
| 1004 | addedGlyphs=addedGlyphs, |
|---|
| 1005 | deletedGlyphs=deletedGlyphs |
|---|
| 1006 | ) |
|---|
| 1007 | |
|---|
| 1008 | def _testFontDataForExternalModifications(self, obj, fileName, reader=None): |
|---|
| 1009 | # font is not on disk |
|---|
| 1010 | if self.path is None: |
|---|
| 1011 | return False |
|---|
| 1012 | # data has not been loaded |
|---|
| 1013 | if obj is None: |
|---|
| 1014 | return |
|---|
| 1015 | # make a reader if necessary |
|---|
| 1016 | if reader is None: |
|---|
| 1017 | reader = UFOReader(self.path) |
|---|
| 1018 | # get the mod time from the reader |
|---|
| 1019 | modTime = reader.getFileModificationTime(fileName) |
|---|
| 1020 | # file is not in the UFO |
|---|
| 1021 | if modTime is None: |
|---|
| 1022 | if obj._dataOnDisk: |
|---|
| 1023 | return True |
|---|
| 1024 | return False |
|---|
| 1025 | # time stamp mismatch |
|---|
| 1026 | if modTime != obj._dataOnDiskTimeStamp: |
|---|
| 1027 | data = reader.readBytesFromPath(fileName) |
|---|
| 1028 | if data != obj._dataOnDisk: |
|---|
| 1029 | return True |
|---|
| 1030 | # fallback |
|---|
| 1031 | return False |
|---|
| 1032 | |
|---|
| 1033 | def _testInfoForExternalModifications(self, reader=None): |
|---|
| 1034 | return self._testFontDataForExternalModifications(self._info, "fontinfo.plist", reader=reader) |
|---|
| 1035 | |
|---|
| 1036 | def _testKerningForExternalModifications(self, reader=None): |
|---|
| 1037 | return self._testFontDataForExternalModifications(self._kerning, "kerning.plist", reader=reader) |
|---|
| 1038 | |
|---|
| 1039 | def _testGroupsForExternalModifications(self, reader=None): |
|---|
| 1040 | return self._testFontDataForExternalModifications(self._groups, "groups.plist", reader=reader) |
|---|
| 1041 | |
|---|
| 1042 | def _testFeaturesForExternalModifications(self, reader=None): |
|---|
| 1043 | return self._testFontDataForExternalModifications(self._features, "features.fea", reader=reader) |
|---|
| 1044 | |
|---|
| 1045 | def _testLibForExternalModifications(self, reader=None): |
|---|
| 1046 | return self._testFontDataForExternalModifications(self._lib, "lib.plist", reader=reader) |
|---|
| 1047 | |
|---|
| 1048 | # data reloading |
|---|
| 1049 | |
|---|
| 1050 | def reloadInfo(self): |
|---|
| 1051 | """ |
|---|
| 1052 | Reload the data in the :class:`Info` object from the |
|---|
| 1053 | fontinfo.plist file in the UFO. |
|---|
| 1054 | """ |
|---|
| 1055 | from ufoLib import deprecatedFontInfoAttributesVersion2 |
|---|
| 1056 | if self._info is None: |
|---|
| 1057 | obj = self.info |
|---|
| 1058 | else: |
|---|
| 1059 | reader = UFOReader(self.path) |
|---|
| 1060 | newInfo = Info() |
|---|
| 1061 | reader.readInfo(newInfo) |
|---|
| 1062 | oldInfo = self._info |
|---|
| 1063 | for attr in dir(newInfo): |
|---|
| 1064 | if attr in deprecatedFontInfoAttributesVersion2: |
|---|
| 1065 | continue |
|---|
| 1066 | if attr.startswith("_"): |
|---|
| 1067 | continue |
|---|
| 1068 | if attr == "dirty": |
|---|
| 1069 | continue |
|---|
| 1070 | if attr == "dispatcher": |
|---|
| 1071 | continue |
|---|
| 1072 | if attr == "font": |
|---|
| 1073 | continue |
|---|
| 1074 | if not hasattr(oldInfo, attr): |
|---|
| 1075 | continue |
|---|
| 1076 | newValue = getattr(newInfo, attr) |
|---|
| 1077 | oldValue = getattr(oldInfo, attr) |
|---|
| 1078 | if hasattr(newValue, "im_func"): |
|---|
| 1079 | continue |
|---|
| 1080 | if oldValue == newValue: |
|---|
| 1081 | continue |
|---|
| 1082 | setattr(oldInfo, attr, newValue) |
|---|
| 1083 | self._stampInfoDataState(reader) |
|---|
| 1084 | |
|---|
| 1085 | def reloadKerning(self): |
|---|
| 1086 | """ |
|---|
| 1087 | Reload the data in the :class:`Kerning` object from the |
|---|
| 1088 | kerning.plist file in the UFO. |
|---|
| 1089 | |
|---|
| 1090 | This validates the kerning against the groups loaded into the |
|---|
| 1091 | font. If groups are being reloaded in the same pass, the groups |
|---|
| 1092 | should always be reloaded before reloading the kerning. |
|---|
| 1093 | """ |
|---|
| 1094 | if self._kerning is None: |
|---|
| 1095 | obj = self.kerning |
|---|
| 1096 | else: |
|---|
| 1097 | reader = UFOReader(self._path) |
|---|
| 1098 | kerning = reader.readKerning() |
|---|
| 1099 | if self._groups is not None: |
|---|
| 1100 | if not kerningValidator(kerning, self._groups): |
|---|
| 1101 | raise DefconError("The kerning data is not valid.") |
|---|
| 1102 | self._kerning.clear() |
|---|
| 1103 | self._kerning.update(kerning) |
|---|
| 1104 | self._stampKerningDataState(reader) |
|---|
| 1105 | |
|---|
| 1106 | def reloadGroups(self): |
|---|
| 1107 | """ |
|---|
| 1108 | Reload the data in the :class:`Groups` object from the |
|---|
| 1109 | groups.plist file in the UFO. |
|---|
| 1110 | """ |
|---|
| 1111 | if self._groups is None: |
|---|
| 1112 | obj = self.groups |
|---|
| 1113 | else: |
|---|
| 1114 | reader = UFOReader(self._path) |
|---|
| 1115 | d = reader.readGroups() |
|---|
| 1116 | self._groups.clear() |
|---|
| 1117 | self._groups.update(d) |
|---|
| 1118 | self._stampGroupsDataState(reader) |
|---|
| 1119 | |
|---|
| 1120 | def reloadFeatures(self): |
|---|
| 1121 | """ |
|---|
| 1122 | Reload the data in the :class:`Features` object from the |
|---|
| 1123 | features.fea file in the UFO. |
|---|
| 1124 | """ |
|---|
| 1125 | if self._features is None: |
|---|
| 1126 | obj = self.features |
|---|
| 1127 | else: |
|---|
| 1128 | reader = UFOReader(self._path) |
|---|
| 1129 | text = reader.readFeatures() |
|---|
| 1130 | self._features.text = text |
|---|
| 1131 | self._stampFeaturesDataState(reader) |
|---|
| 1132 | |
|---|
| 1133 | def reloadLib(self): |
|---|
| 1134 | """ |
|---|
| 1135 | Reload the data in the :class:`Lib` object from the |
|---|
| 1136 | lib.plist file in the UFO. |
|---|
| 1137 | """ |
|---|
| 1138 | if self._lib is None: |
|---|
| 1139 | obj = self.lib |
|---|
| 1140 | else: |
|---|
| 1141 | reader = UFOReader(self._path) |
|---|
| 1142 | d = reader.readLib() |
|---|
| 1143 | self._lib.clear() |
|---|
| 1144 | self._lib.update(d) |
|---|
| 1145 | self._stampLibDataState(reader) |
|---|
| 1146 | |
|---|
| 1147 | def reloadImages(self, fileNames): |
|---|
| 1148 | """ |
|---|
| 1149 | Reload the images listed in **fileNames** from the |
|---|
| 1150 | appropriate files within the UFO. When all of the |
|---|
| 1151 | loading is complete, a *Font.ReloadedImages* notification |
|---|
| 1152 | will be posted. |
|---|
| 1153 | """ |
|---|
| 1154 | self.images.reloadImages(fileNames) |
|---|
| 1155 | self.postNotification(notification="Font.ReloadedImages") |
|---|
| 1156 | |
|---|
| 1157 | def reloadData(self, fileNames): |
|---|
| 1158 | """ |
|---|
| 1159 | Reload the data files listed in **fileNames** from the |
|---|
| 1160 | appropriate files within the UFO. When all of the |
|---|
| 1161 | loading is complete, a *Font.ReloadedData* notification |
|---|
| 1162 | will be posted. |
|---|
| 1163 | """ |
|---|
| 1164 | self.images.reloadImages(fileNames) |
|---|
| 1165 | self.postNotification(notification="Font.ReloadedData") |
|---|
| 1166 | |
|---|
| 1167 | def reloadGlyphs(self, glyphNames): |
|---|
| 1168 | """ |
|---|
| 1169 | Deprecated! Use reloadLayers! |
|---|
| 1170 | |
|---|
| 1171 | Reload the glyphs listed in **glyphNames** from the |
|---|
| 1172 | appropriate files within the UFO. When all of the |
|---|
| 1173 | loading is complete, a *Font.ReloadedGlyphs* notification |
|---|
| 1174 | will be posted. |
|---|
| 1175 | """ |
|---|
| 1176 | defaultLayerName = self.layers.defaultLayer.name |
|---|
| 1177 | layerData = dict( |
|---|
| 1178 | layers={ |
|---|
| 1179 | defaultLayerName : dict(glyphNames=glyphNames) |
|---|
| 1180 | } |
|---|
| 1181 | ) |
|---|
| 1182 | self.reloadLayers(layerData) |
|---|
| 1183 | |
|---|
| 1184 | def reloadLayers(self, layerData): |
|---|
| 1185 | """ |
|---|
| 1186 | Reload the data in the layers specfied in **layerData**. |
|---|
| 1187 | When all of the loading is complete, *Font.ReloadedLayers* |
|---|
| 1188 | and *Font.ReloadedGlyphs* notifications will be posted. |
|---|
| 1189 | The **layerData** must be a dictionary following this format:: |
|---|
| 1190 | |
|---|
| 1191 | { |
|---|
| 1192 | "order" : bool, # True if you want the order releaded |
|---|
| 1193 | "default" : bool, # True if you want the default layer reset |
|---|
| 1194 | "layers" : { |
|---|
| 1195 | "layer name" : { |
|---|
| 1196 | "glyphNames" : ["glyph name 1", "glyph name 2"], # list of glyph names you want to reload |
|---|
| 1197 | "info" : bool, # True if you want the layer info reloaded |
|---|
| 1198 | } |
|---|
| 1199 | } |
|---|
| 1200 | } |
|---|
| 1201 | """ |
|---|
| 1202 | self.layers.reloadLayers(layerData) |
|---|
| 1203 | self.postNotification(notification="Font.ReloadedLayers") |
|---|
| 1204 | self.postNotification(notification="Font.ReloadedGlyphs") |
|---|
| 1205 | |
|---|
| 1206 | |
|---|
| 1207 | # ----------------------------- |
|---|
| 1208 | # UFO Format Version Conversion |
|---|
| 1209 | # ----------------------------- |
|---|
| 1210 | |
|---|
| 1211 | def _convertFromFormatVersion1RoboFabData(self): |
|---|
| 1212 | # migrate features from the lib |
|---|
| 1213 | features = [] |
|---|
| 1214 | classes = self.lib.get("org.robofab.opentype.classes") |
|---|
| 1215 | if classes is not None: |
|---|
| 1216 | del self.lib["org.robofab.opentype.classes"] |
|---|
| 1217 | features.append(classes) |
|---|
| 1218 | splitFeatures = self.lib.get("org.robofab.opentype.features") |
|---|
| 1219 | if splitFeatures is not None: |
|---|
| 1220 | order = self.lib.get("org.robofab.opentype.featureorder") |
|---|
| 1221 | if order is None: |
|---|
| 1222 | order = splitFeatures.keys() |
|---|
| 1223 | order.sort() |
|---|
| 1224 | else: |
|---|
| 1225 | del self.lib["org.robofab.opentype.featureorder"] |
|---|
| 1226 | del self.lib["org.robofab.opentype.features"] |
|---|
| 1227 | for tag in order: |
|---|
| 1228 | oneFeature = splitFeatures.get(tag) |
|---|
| 1229 | if oneFeature is not None: |
|---|
| 1230 | features.append(oneFeature) |
|---|
| 1231 | self.features.text = "\n".join(features) |
|---|
| 1232 | # migrate hint data from the lib |
|---|
| 1233 | hintData = self.lib.get("org.robofab.postScriptHintData") |
|---|
| 1234 | if hintData is not None: |
|---|
| 1235 | del self.lib["org.robofab.postScriptHintData"] |
|---|
| 1236 | # settings |
|---|
| 1237 | blueFuzz = hintData.get("blueFuzz") |
|---|
| 1238 | if blueFuzz is not None: |
|---|
| 1239 | self.info.postscriptBlueFuzz = blueFuzz |
|---|
| 1240 | blueScale = hintData.get("blueScale") |
|---|
| 1241 | if blueScale is not None: |
|---|
| 1242 | self.info.postscriptBlueScale = blueScale |
|---|
| 1243 | blueShift = hintData.get("blueShift") |
|---|
| 1244 | if blueShift is not None: |
|---|
| 1245 | self.info.postscriptBlueShift = blueShift |
|---|
| 1246 | forceBold = hintData.get("forceBold") |
|---|
| 1247 | if forceBold is not None: |
|---|
| 1248 | self.info.postscriptForceBold = forceBold |
|---|
| 1249 | # stems |
|---|
| 1250 | vStems = hintData.get("vStems") |
|---|
| 1251 | if vStems is not None: |
|---|
| 1252 | self.info.postscriptStemSnapV = vStems |
|---|
| 1253 | hStems = hintData.get("hStems") |
|---|
| 1254 | if hStems is not None: |
|---|
| 1255 | self.info.postscriptStemSnapH = hStems |
|---|
| 1256 | # blues |
|---|
| 1257 | bluePairs = [ |
|---|
| 1258 | ("postscriptBlueValues", "blueValues"), |
|---|
| 1259 | ("postscriptOtherBlues", "otherBlues"), |
|---|
| 1260 | ("postscriptFamilyBlues", "familyBlues"), |
|---|
| 1261 | ("postscriptFamilyOtherBlues", "familyOtherBlues"), |
|---|
| 1262 | ] |
|---|
| 1263 | for infoAttr, libKey in bluePairs: |
|---|
| 1264 | libValue = hintData.get(libKey) |
|---|
| 1265 | if libValue is not None: |
|---|
| 1266 | value = [] |
|---|
| 1267 | for i, j in libValue: |
|---|
| 1268 | value.append(i) |
|---|
| 1269 | value.append(j) |
|---|
| 1270 | setattr(self.info, infoAttr, value) |
|---|
| 1271 | |
|---|
| 1272 | def _convertToFormatVersion1RoboFabData(self, libCopy): |
|---|
| 1273 | from robofab.tools.fontlabFeatureSplitter import splitFeaturesForFontLab |
|---|
| 1274 | # features |
|---|
| 1275 | features = self.features.text |
|---|
| 1276 | classes, features = splitFeaturesForFontLab(features) |
|---|
| 1277 | if classes: |
|---|
| 1278 | libCopy["org.robofab.opentype.classes"] = classes.strip() + "\n" |
|---|
| 1279 | if features: |
|---|
| 1280 | featureDict = {} |
|---|
| 1281 | for featureName, featureText in features: |
|---|
| 1282 | featureDict[featureName] = featureText.strip() + "\n" |
|---|
| 1283 | libCopy["org.robofab.opentype.features"] = featureDict |
|---|
| 1284 | libCopy["org.robofab.opentype.featureorder"] = [featureName for featureName, featureText in features] |
|---|
| 1285 | # hint data |
|---|
| 1286 | hintData = dict( |
|---|
| 1287 | blueFuzz=self.info.postscriptBlueFuzz, |
|---|
| 1288 | blueScale=self.info.postscriptBlueScale, |
|---|
| 1289 | blueShift=self.info.postscriptBlueShift, |
|---|
| 1290 | forceBold=self.info.postscriptForceBold, |
|---|
| 1291 | vStems=self.info.postscriptStemSnapV, |
|---|
| 1292 | hStems=self.info.postscriptStemSnapH |
|---|
| 1293 | ) |
|---|
| 1294 | bluePairs = [ |
|---|
| 1295 | ("postscriptBlueValues", "blueValues"), |
|---|
| 1296 | ("postscriptOtherBlues", "otherBlues"), |
|---|
| 1297 | ("postscriptFamilyBlues", "familyBlues"), |
|---|
| 1298 | ("postscriptFamilyOtherBlues", "familyOtherBlues"), |
|---|
| 1299 | ] |
|---|
| 1300 | for infoAttr, libKey in bluePairs: |
|---|
| 1301 | values = getattr(self.info, infoAttr) |
|---|
| 1302 | if values is not None: |
|---|
| 1303 | finalValues = [] |
|---|
| 1304 | for value in values: |
|---|
| 1305 | if not finalValues or len(finalValues[-1]) == 2: |
|---|
| 1306 | finalValues.append([]) |
|---|
| 1307 | finalValues[-1].append(value) |
|---|
| 1308 | hintData[libKey] = finalValues |
|---|
| 1309 | for key, value in hintData.items(): |
|---|
| 1310 | if value is None: |
|---|
| 1311 | del hintData[key] |
|---|
| 1312 | libCopy["org.robofab.postScriptHintData"] = hintData |
|---|
| 1313 | |
|---|
| 1314 | |
|---|
| 1315 | # ----- |
|---|
| 1316 | # Tests |
|---|
| 1317 | # ----- |
|---|
| 1318 | |
|---|
| 1319 | def _testSetParentDataInGlyph(): |
|---|
| 1320 | """ |
|---|
| 1321 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1322 | >>> font = Font(getTestFontPath()) |
|---|
| 1323 | >>> glyph = font['A'] |
|---|
| 1324 | >>> id(glyph.getParent()) == id(font) |
|---|
| 1325 | True |
|---|
| 1326 | """ |
|---|
| 1327 | |
|---|
| 1328 | def _testNewGlyph(): |
|---|
| 1329 | """ |
|---|
| 1330 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1331 | >>> font = Font(getTestFontPath()) |
|---|
| 1332 | >>> font.newGlyph('NewGlyphTest') |
|---|
| 1333 | >>> glyph = font['NewGlyphTest'] |
|---|
| 1334 | >>> glyph.name |
|---|
| 1335 | 'NewGlyphTest' |
|---|
| 1336 | >>> glyph.dirty |
|---|
| 1337 | True |
|---|
| 1338 | >>> font.dirty |
|---|
| 1339 | True |
|---|
| 1340 | >>> keys = font.keys() |
|---|
| 1341 | >>> keys.sort() |
|---|
| 1342 | >>> keys |
|---|
| 1343 | ['A', 'B', 'C', 'NewGlyphTest'] |
|---|
| 1344 | """ |
|---|
| 1345 | |
|---|
| 1346 | def _testIter(): |
|---|
| 1347 | """ |
|---|
| 1348 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1349 | >>> font = Font(getTestFontPath()) |
|---|
| 1350 | >>> names = [glyph.name for glyph in font] |
|---|
| 1351 | >>> names.sort() |
|---|
| 1352 | >>> names |
|---|
| 1353 | ['A', 'B', 'C'] |
|---|
| 1354 | >>> names = [] |
|---|
| 1355 | >>> for glyph1 in font: |
|---|
| 1356 | ... for glyph2 in font: |
|---|
| 1357 | ... names.append((glyph1.name, glyph2.name)) |
|---|
| 1358 | >>> names.sort() |
|---|
| 1359 | >>> names |
|---|
| 1360 | [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')] |
|---|
| 1361 | """ |
|---|
| 1362 | |
|---|
| 1363 | def _testGetitem(): |
|---|
| 1364 | """ |
|---|
| 1365 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1366 | >>> font = Font(getTestFontPath()) |
|---|
| 1367 | >>> font['A'].name |
|---|
| 1368 | 'A' |
|---|
| 1369 | >>> font['B'].name |
|---|
| 1370 | 'B' |
|---|
| 1371 | >>> font['NotInFont'] |
|---|
| 1372 | Traceback (most recent call last): |
|---|
| 1373 | ... |
|---|
| 1374 | KeyError: 'NotInFont not in layer' |
|---|
| 1375 | """ |
|---|
| 1376 | |
|---|
| 1377 | def _testDelitem(): |
|---|
| 1378 | """ |
|---|
| 1379 | >>> from defcon.test.testTools import makeTestFontCopy, tearDownTestFontCopy |
|---|
| 1380 | >>> import glob |
|---|
| 1381 | >>> import os |
|---|
| 1382 | >>> path = makeTestFontCopy() |
|---|
| 1383 | >>> font = Font(path) |
|---|
| 1384 | >>> del font['A'] |
|---|
| 1385 | >>> font.dirty |
|---|
| 1386 | True |
|---|
| 1387 | >>> font.newGlyph('NewGlyphTest') |
|---|
| 1388 | >>> del font['NewGlyphTest'] |
|---|
| 1389 | >>> keys = font.keys() |
|---|
| 1390 | >>> keys.sort() |
|---|
| 1391 | >>> keys |
|---|
| 1392 | ['B', 'C'] |
|---|
| 1393 | >>> len(font) |
|---|
| 1394 | 2 |
|---|
| 1395 | >>> 'A' in font |
|---|
| 1396 | False |
|---|
| 1397 | >>> font.save() |
|---|
| 1398 | >>> fileNames = glob.glob(os.path.join(path, 'Glyphs', '*.glif')) |
|---|
| 1399 | >>> fileNames = [os.path.basename(fileName) for fileName in fileNames] |
|---|
| 1400 | >>> fileNames.sort() |
|---|
| 1401 | >>> fileNames |
|---|
| 1402 | ['B_.glif', 'C_.glif'] |
|---|
| 1403 | >>> del font['NotInFont'] |
|---|
| 1404 | Traceback (most recent call last): |
|---|
| 1405 | ... |
|---|
| 1406 | KeyError: 'NotInFont not in layer' |
|---|
| 1407 | >>> tearDownTestFontCopy() |
|---|
| 1408 | |
|---|
| 1409 | # # test saving externally deleted glyphs. |
|---|
| 1410 | # # del glyph. not dirty. |
|---|
| 1411 | # >>> path = makeTestFontCopy() |
|---|
| 1412 | # >>> font = Font(path) |
|---|
| 1413 | # >>> glyph = font["A"] |
|---|
| 1414 | # >>> glyphPath = os.path.join(path, "glyphs", "A_.glif") |
|---|
| 1415 | # >>> os.remove(glyphPath) |
|---|
| 1416 | # >>> r = font.testForExternalChanges() |
|---|
| 1417 | # >>> r["deletedGlyphs"] |
|---|
| 1418 | # ['A'] |
|---|
| 1419 | # >>> del font["A"] |
|---|
| 1420 | # >>> font.save() |
|---|
| 1421 | # >>> os.path.exists(glyphPath) |
|---|
| 1422 | # False |
|---|
| 1423 | # >>> tearDownTestFontCopy() |
|---|
| 1424 | |
|---|
| 1425 | # # del glyph. dirty. |
|---|
| 1426 | # >>> path = makeTestFontCopy() |
|---|
| 1427 | # >>> font = Font(path) |
|---|
| 1428 | # >>> glyph = font["A"] |
|---|
| 1429 | # >>> glyph.dirty = True |
|---|
| 1430 | # >>> glyphPath = os.path.join(path, "glyphs", "A_.glif") |
|---|
| 1431 | # >>> os.remove(glyphPath) |
|---|
| 1432 | # >>> r = font.testForExternalChanges() |
|---|
| 1433 | # >>> r["deletedGlyphs"] |
|---|
| 1434 | # ['A'] |
|---|
| 1435 | # >>> del font["A"] |
|---|
| 1436 | # >>> font.save() |
|---|
| 1437 | # >>> os.path.exists(glyphPath) |
|---|
| 1438 | # False |
|---|
| 1439 | # >>> tearDownTestFontCopy() |
|---|
| 1440 | """ |
|---|
| 1441 | |
|---|
| 1442 | def _testLen(): |
|---|
| 1443 | """ |
|---|
| 1444 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1445 | >>> font = Font(getTestFontPath()) |
|---|
| 1446 | >>> len(font) |
|---|
| 1447 | 3 |
|---|
| 1448 | |
|---|
| 1449 | >>> font = Font() |
|---|
| 1450 | >>> len(font) |
|---|
| 1451 | 0 |
|---|
| 1452 | """ |
|---|
| 1453 | |
|---|
| 1454 | def _testContains(): |
|---|
| 1455 | """ |
|---|
| 1456 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1457 | >>> font = Font(getTestFontPath()) |
|---|
| 1458 | >>> 'A' in font |
|---|
| 1459 | True |
|---|
| 1460 | >>> 'NotInFont' in font |
|---|
| 1461 | False |
|---|
| 1462 | |
|---|
| 1463 | >>> font = Font() |
|---|
| 1464 | >>> 'A' in font |
|---|
| 1465 | False |
|---|
| 1466 | """ |
|---|
| 1467 | |
|---|
| 1468 | def _testKeys(): |
|---|
| 1469 | """ |
|---|
| 1470 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1471 | >>> font = Font(getTestFontPath()) |
|---|
| 1472 | >>> keys = font.keys() |
|---|
| 1473 | >>> keys.sort() |
|---|
| 1474 | >>> print keys |
|---|
| 1475 | ['A', 'B', 'C'] |
|---|
| 1476 | >>> del font["A"] |
|---|
| 1477 | >>> keys = font.keys() |
|---|
| 1478 | >>> keys.sort() |
|---|
| 1479 | >>> print keys |
|---|
| 1480 | ['B', 'C'] |
|---|
| 1481 | >>> font.newGlyph("A") |
|---|
| 1482 | >>> keys = font.keys() |
|---|
| 1483 | >>> keys.sort() |
|---|
| 1484 | >>> print keys |
|---|
| 1485 | ['A', 'B', 'C'] |
|---|
| 1486 | |
|---|
| 1487 | >>> font = Font() |
|---|
| 1488 | >>> font.keys() |
|---|
| 1489 | [] |
|---|
| 1490 | >>> font.newGlyph("A") |
|---|
| 1491 | >>> keys = font.keys() |
|---|
| 1492 | >>> keys.sort() |
|---|
| 1493 | >>> print keys |
|---|
| 1494 | ['A'] |
|---|
| 1495 | """ |
|---|
| 1496 | |
|---|
| 1497 | def _testPath(): |
|---|
| 1498 | """ |
|---|
| 1499 | # get |
|---|
| 1500 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1501 | >>> path = getTestFontPath() |
|---|
| 1502 | >>> font = Font(path) |
|---|
| 1503 | >>> font.path == path |
|---|
| 1504 | True |
|---|
| 1505 | |
|---|
| 1506 | >>> font = Font() |
|---|
| 1507 | >>> font.path == None |
|---|
| 1508 | True |
|---|
| 1509 | |
|---|
| 1510 | # set |
|---|
| 1511 | >>> import shutil |
|---|
| 1512 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1513 | >>> path1 = getTestFontPath() |
|---|
| 1514 | >>> font = Font(path1) |
|---|
| 1515 | >>> path2 = getTestFontPath("setPathTest.ufo") |
|---|
| 1516 | >>> shutil.copytree(path1, path2) |
|---|
| 1517 | >>> font.path = path2 |
|---|
| 1518 | >>> shutil.rmtree(path2) |
|---|
| 1519 | """ |
|---|
| 1520 | |
|---|
| 1521 | def _testGlyphWithOutlines(): |
|---|
| 1522 | """ |
|---|
| 1523 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1524 | >>> font = Font(getTestFontPath()) |
|---|
| 1525 | >>> sorted(font.glyphsWithOutlines) |
|---|
| 1526 | ['A', 'B'] |
|---|
| 1527 | >>> font = Font(getTestFontPath()) |
|---|
| 1528 | >>> for glyph in font: |
|---|
| 1529 | ... pass |
|---|
| 1530 | >>> sorted(font.glyphsWithOutlines) |
|---|
| 1531 | ['A', 'B'] |
|---|
| 1532 | """ |
|---|
| 1533 | |
|---|
| 1534 | def _testComponentReferences(): |
|---|
| 1535 | """ |
|---|
| 1536 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1537 | >>> font = Font(getTestFontPath()) |
|---|
| 1538 | >>> font.componentReferences |
|---|
| 1539 | {'A': set(['C']), 'B': set(['C'])} |
|---|
| 1540 | >>> glyph = font["C"] |
|---|
| 1541 | >>> font.componentReferences |
|---|
| 1542 | {'A': set(['C']), 'B': set(['C'])} |
|---|
| 1543 | """ |
|---|
| 1544 | |
|---|
| 1545 | def _testBounds(): |
|---|
| 1546 | """ |
|---|
| 1547 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1548 | >>> font = Font(getTestFontPath()) |
|---|
| 1549 | >>> font.bounds |
|---|
| 1550 | (0, 0, 700, 700) |
|---|
| 1551 | """ |
|---|
| 1552 | |
|---|
| 1553 | def _testControlPointBounds(): |
|---|
| 1554 | """ |
|---|
| 1555 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1556 | >>> font = Font(getTestFontPath()) |
|---|
| 1557 | >>> font.controlPointBounds |
|---|
| 1558 | (0, 0, 700, 700) |
|---|
| 1559 | """ |
|---|
| 1560 | |
|---|
| 1561 | def _testSave(): |
|---|
| 1562 | """ |
|---|
| 1563 | >>> from defcon.test.testTools import makeTestFontCopy, tearDownTestFontCopy, getTestFontPath, getTestFontCopyPath |
|---|
| 1564 | >>> import glob |
|---|
| 1565 | >>> import os |
|---|
| 1566 | >>> path = makeTestFontCopy() |
|---|
| 1567 | >>> font = Font(path) |
|---|
| 1568 | >>> for glyph in font: |
|---|
| 1569 | ... glyph.dirty = True |
|---|
| 1570 | >>> font.save() |
|---|
| 1571 | >>> fileNames = glob.glob(os.path.join(path, 'Glyphs', '*.glif')) |
|---|
| 1572 | >>> fileNames = [os.path.basename(fileName) for fileName in fileNames] |
|---|
| 1573 | >>> fileNames.sort() |
|---|
| 1574 | >>> fileNames |
|---|
| 1575 | ['A_.glif', 'B_.glif', 'C_.glif'] |
|---|
| 1576 | >>> tearDownTestFontCopy() |
|---|
| 1577 | |
|---|
| 1578 | >>> path = getTestFontPath() |
|---|
| 1579 | >>> font = Font(path) |
|---|
| 1580 | >>> saveAsPath = getTestFontCopyPath(path) |
|---|
| 1581 | >>> font.save(saveAsPath) |
|---|
| 1582 | >>> fileNames = glob.glob(os.path.join(saveAsPath, 'Glyphs', '*.glif')) |
|---|
| 1583 | >>> fileNames = [os.path.basename(fileName) for fileName in fileNames] |
|---|
| 1584 | >>> fileNames.sort() |
|---|
| 1585 | >>> fileNames |
|---|
| 1586 | ['A_.glif', 'B_.glif', 'C_.glif'] |
|---|
| 1587 | >>> font.path == saveAsPath |
|---|
| 1588 | True |
|---|
| 1589 | >>> tearDownTestFontCopy(saveAsPath) |
|---|
| 1590 | """ |
|---|
| 1591 | |
|---|
| 1592 | def _testGlyphNameChange(): |
|---|
| 1593 | """ |
|---|
| 1594 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1595 | >>> font = Font(getTestFontPath()) |
|---|
| 1596 | >>> glyph = font['A'] |
|---|
| 1597 | >>> glyph.name = 'NameChangeTest' |
|---|
| 1598 | >>> keys = font.keys() |
|---|
| 1599 | >>> keys.sort() |
|---|
| 1600 | >>> keys |
|---|
| 1601 | ['B', 'C', 'NameChangeTest'] |
|---|
| 1602 | >>> font.dirty |
|---|
| 1603 | True |
|---|
| 1604 | """ |
|---|
| 1605 | |
|---|
| 1606 | def _testGlyphUnicodesChanged(): |
|---|
| 1607 | """ |
|---|
| 1608 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1609 | >>> font = Font(getTestFontPath()) |
|---|
| 1610 | >>> glyph = font['A'] |
|---|
| 1611 | >>> glyph.unicodes = [123, 456] |
|---|
| 1612 | >>> font.unicodeData[123] |
|---|
| 1613 | ['A'] |
|---|
| 1614 | >>> font.unicodeData[456] |
|---|
| 1615 | ['A'] |
|---|
| 1616 | >>> font.unicodeData[66] |
|---|
| 1617 | ['B'] |
|---|
| 1618 | >>> font.unicodeData.get(65) |
|---|
| 1619 | |
|---|
| 1620 | >>> font = Font(getTestFontPath()) |
|---|
| 1621 | >>> font.newGlyph("test") |
|---|
| 1622 | >>> glyph = font["test"] |
|---|
| 1623 | >>> glyph.unicodes = [65] |
|---|
| 1624 | >>> font.unicodeData[65] |
|---|
| 1625 | ['test', 'A'] |
|---|
| 1626 | """ |
|---|
| 1627 | |
|---|
| 1628 | def _testTestForExternalChanges(): |
|---|
| 1629 | """ |
|---|
| 1630 | >>> from plistlib import readPlist, writePlist |
|---|
| 1631 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1632 | >>> path = getTestFontPath("TestExternalEditing.ufo") |
|---|
| 1633 | >>> font = Font(path) |
|---|
| 1634 | |
|---|
| 1635 | # load all the objects so that they get stamped |
|---|
| 1636 | >>> i = font.info |
|---|
| 1637 | >>> k = font.kerning |
|---|
| 1638 | >>> g = font.groups |
|---|
| 1639 | >>> l = font.lib |
|---|
| 1640 | >>> g = font["A"] |
|---|
| 1641 | |
|---|
| 1642 | >>> d = font.testForExternalChanges() |
|---|
| 1643 | >>> d["info"] |
|---|
| 1644 | False |
|---|
| 1645 | >>> d["kerning"] |
|---|
| 1646 | False |
|---|
| 1647 | >>> d["groups"] |
|---|
| 1648 | False |
|---|
| 1649 | >>> d["lib"] |
|---|
| 1650 | False |
|---|
| 1651 | |
|---|
| 1652 | # make a simple change to the kerning data |
|---|
| 1653 | >>> path = os.path.join(font.path, "kerning.plist") |
|---|
| 1654 | >>> f = open(path, "rb") |
|---|
| 1655 | >>> t = f.read() |
|---|
| 1656 | >>> f.close() |
|---|
| 1657 | >>> t += " " |
|---|
| 1658 | >>> f = open(path, "wb") |
|---|
| 1659 | >>> f.write(t) |
|---|
| 1660 | >>> f.close() |
|---|
| 1661 | >>> os.utime(path, (k._dataOnDiskTimeStamp + 1, k._dataOnDiskTimeStamp + 1)) |
|---|
| 1662 | |
|---|
| 1663 | >>> d = font.testForExternalChanges() |
|---|
| 1664 | >>> d["kerning"] |
|---|
| 1665 | True |
|---|
| 1666 | >>> d["info"] |
|---|
| 1667 | False |
|---|
| 1668 | |
|---|
| 1669 | # save the kerning data and test again |
|---|
| 1670 | >>> font.kerning.dirty = True |
|---|
| 1671 | >>> font.save() |
|---|
| 1672 | >>> d = font.testForExternalChanges() |
|---|
| 1673 | >>> d["kerning"] |
|---|
| 1674 | False |
|---|
| 1675 | """ |
|---|
| 1676 | |
|---|
| 1677 | def _testReloadInfo(): |
|---|
| 1678 | """ |
|---|
| 1679 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1680 | >>> path = getTestFontPath("TestExternalEditing.ufo") |
|---|
| 1681 | >>> font = Font(path) |
|---|
| 1682 | >>> info = font.info |
|---|
| 1683 | |
|---|
| 1684 | >>> path = os.path.join(font.path, "fontinfo.plist") |
|---|
| 1685 | >>> f = open(path, "rb") |
|---|
| 1686 | >>> t = f.read() |
|---|
| 1687 | >>> f.close() |
|---|
| 1688 | >>> t = t.replace("<integer>750</integer>", "<integer>751</integer>") |
|---|
| 1689 | >>> f = open(path, "wb") |
|---|
| 1690 | >>> f.write(t) |
|---|
| 1691 | >>> f.close() |
|---|
| 1692 | |
|---|
| 1693 | >>> info.ascender |
|---|
| 1694 | 750 |
|---|
| 1695 | >>> font.reloadInfo() |
|---|
| 1696 | >>> info.ascender |
|---|
| 1697 | 751 |
|---|
| 1698 | |
|---|
| 1699 | >>> t = t.replace("<integer>751</integer>", "<integer>750</integer>") |
|---|
| 1700 | >>> f = open(path, "wb") |
|---|
| 1701 | >>> f.write(t) |
|---|
| 1702 | >>> f.close() |
|---|
| 1703 | """ |
|---|
| 1704 | |
|---|
| 1705 | def _testReloadKerning(): |
|---|
| 1706 | """ |
|---|
| 1707 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1708 | >>> path = getTestFontPath("TestExternalEditing.ufo") |
|---|
| 1709 | >>> font = Font(path) |
|---|
| 1710 | >>> kerning = font.kerning |
|---|
| 1711 | |
|---|
| 1712 | >>> path = os.path.join(font.path, "kerning.plist") |
|---|
| 1713 | >>> f = open(path, "rb") |
|---|
| 1714 | >>> t = f.read() |
|---|
| 1715 | >>> f.close() |
|---|
| 1716 | >>> t = t.replace("<integer>-100</integer>", "<integer>-101</integer>") |
|---|
| 1717 | >>> f = open(path, "wb") |
|---|
| 1718 | >>> f.write(t) |
|---|
| 1719 | >>> f.close() |
|---|
| 1720 | |
|---|
| 1721 | >>> kerning.items() |
|---|
| 1722 | [(('A', 'A'), -100)] |
|---|
| 1723 | >>> font.reloadKerning() |
|---|
| 1724 | >>> kerning.items() |
|---|
| 1725 | [(('A', 'A'), -101)] |
|---|
| 1726 | |
|---|
| 1727 | >>> t = t.replace("<integer>-101</integer>", "<integer>-100</integer>") |
|---|
| 1728 | >>> f = open(path, "wb") |
|---|
| 1729 | >>> f.write(t) |
|---|
| 1730 | >>> f.close() |
|---|
| 1731 | """ |
|---|
| 1732 | |
|---|
| 1733 | def _testReloadGroups(): |
|---|
| 1734 | """ |
|---|
| 1735 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1736 | >>> path = getTestFontPath("TestExternalEditing.ufo") |
|---|
| 1737 | >>> font = Font(path) |
|---|
| 1738 | >>> groups = font.groups |
|---|
| 1739 | |
|---|
| 1740 | >>> path = os.path.join(font.path, "groups.plist") |
|---|
| 1741 | >>> f = open(path, "rb") |
|---|
| 1742 | >>> t = f.read() |
|---|
| 1743 | >>> f.close() |
|---|
| 1744 | >>> t = t.replace("<key>TestGroup</key>", "<key>XXX</key>") |
|---|
| 1745 | >>> f = open(path, "wb") |
|---|
| 1746 | >>> f.write(t) |
|---|
| 1747 | >>> f.close() |
|---|
| 1748 | |
|---|
| 1749 | >>> groups.keys() |
|---|
| 1750 | ['TestGroup'] |
|---|
| 1751 | >>> font.reloadGroups() |
|---|
| 1752 | >>> groups.keys() |
|---|
| 1753 | ['XXX'] |
|---|
| 1754 | |
|---|
| 1755 | >>> t = t.replace("<key>XXX</key>", "<key>TestGroup</key>") |
|---|
| 1756 | >>> f = open(path, "wb") |
|---|
| 1757 | >>> f.write(t) |
|---|
| 1758 | >>> f.close() |
|---|
| 1759 | """ |
|---|
| 1760 | |
|---|
| 1761 | def _testReloadLib(): |
|---|
| 1762 | """ |
|---|
| 1763 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1764 | >>> path = getTestFontPath("TestExternalEditing.ufo") |
|---|
| 1765 | >>> font = Font(path) |
|---|
| 1766 | >>> lib = font.lib |
|---|
| 1767 | |
|---|
| 1768 | >>> path = os.path.join(font.path, "lib.plist") |
|---|
| 1769 | >>> f = open(path, "rb") |
|---|
| 1770 | >>> t = f.read() |
|---|
| 1771 | >>> f.close() |
|---|
| 1772 | >>> t = t.replace("<key>org.robofab.glyphOrder</key>", "<key>org.robofab.glyphOrder.XXX</key>") |
|---|
| 1773 | >>> f = open(path, "wb") |
|---|
| 1774 | >>> f.write(t) |
|---|
| 1775 | >>> f.close() |
|---|
| 1776 | |
|---|
| 1777 | >>> lib.keys() |
|---|
| 1778 | ['org.robofab.glyphOrder'] |
|---|
| 1779 | >>> font.reloadLib() |
|---|
| 1780 | >>> lib.keys() |
|---|
| 1781 | ['org.robofab.postScriptHintData', 'org.robofab.glyphOrder.XXX'] |
|---|
| 1782 | |
|---|
| 1783 | >>> t = t.replace("<key>org.robofab.glyphOrder.XXX</key>", "<key>org.robofab.glyphOrder</key>") |
|---|
| 1784 | >>> f = open(path, "wb") |
|---|
| 1785 | >>> f.write(t) |
|---|
| 1786 | >>> f.close() |
|---|
| 1787 | """ |
|---|
| 1788 | |
|---|
| 1789 | def _testReloadGlyphs(): |
|---|
| 1790 | """ |
|---|
| 1791 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1792 | >>> path = getTestFontPath("TestExternalEditing.ufo") |
|---|
| 1793 | >>> font = Font(path) |
|---|
| 1794 | >>> glyph = font["A"] |
|---|
| 1795 | |
|---|
| 1796 | >>> path = os.path.join(font.path, "glyphs", "A_.glif") |
|---|
| 1797 | >>> f = open(path, "rb") |
|---|
| 1798 | >>> t = f.read() |
|---|
| 1799 | >>> f.close() |
|---|
| 1800 | >>> t = t.replace('<advance width="700"/>', '<advance width="701"/>') |
|---|
| 1801 | >>> f = open(path, "wb") |
|---|
| 1802 | >>> f.write(t) |
|---|
| 1803 | >>> f.close() |
|---|
| 1804 | |
|---|
| 1805 | >>> glyph.width |
|---|
| 1806 | 700 |
|---|
| 1807 | >>> len(glyph) |
|---|
| 1808 | 2 |
|---|
| 1809 | >>> font.reloadGlyphs(["A"]) |
|---|
| 1810 | >>> glyph.width |
|---|
| 1811 | 701 |
|---|
| 1812 | >>> len(glyph) |
|---|
| 1813 | 2 |
|---|
| 1814 | |
|---|
| 1815 | >>> t = t.replace('<advance width="701"/>', '<advance width="700"/>') |
|---|
| 1816 | >>> f = open(path, "wb") |
|---|
| 1817 | >>> f.write(t) |
|---|
| 1818 | >>> f.close() |
|---|
| 1819 | """ |
|---|
| 1820 | |
|---|
| 1821 | def _testGlyphOrder(): |
|---|
| 1822 | """ |
|---|
| 1823 | >>> from defcon.test.testTools import getTestFontPath |
|---|
| 1824 | >>> font = Font(getTestFontPath()) |
|---|
| 1825 | >>> font.glyphOrder |
|---|
| 1826 | [] |
|---|
| 1827 | >>> font.glyphOrder = list(sorted(font.keys())) |
|---|
| 1828 | >>> font.glyphOrder |
|---|
| 1829 | ['A', 'B', 'C'] |
|---|
| 1830 | >>> layer = font.layers["public.default"] |
|---|
| 1831 | >>> layer.newGlyph("X") |
|---|
| 1832 | >>> font.glyphOrder |
|---|
| 1833 | ['A', 'B', 'C', 'X'] |
|---|
| 1834 | >>> del layer["A"] |
|---|
| 1835 | >>> font.glyphOrder |
|---|
| 1836 | ['A', 'B', 'C', 'X'] |
|---|
| 1837 | >>> del layer["X"] |
|---|
| 1838 | >>> font.glyphOrder |
|---|
| 1839 | ['A', 'B', 'C'] |
|---|
| 1840 | """ |
|---|
| 1841 | |
|---|
| 1842 | if __name__ == "__main__": |
|---|
| 1843 | import doctest |
|---|
| 1844 | doctest.testmod() |
|---|