source: packages/defcon/branches/ufo3/Lib/defcon/objects/font.py @ 1069

Revision 1069, 63.3 KB checked in by tal, 16 months ago (diff)
Undefined variable.
Line 
1import os
2import re
3import weakref
4from copy import deepcopy
5import tempfile
6import shutil
7from fontTools.misc.arrayTools import unionRect
8from ufoLib import UFOReader, UFOWriter
9from ufoLib.validators import kerningValidator
10from defcon.errors import DefconError
11from defcon.objects.base import BaseObject
12from defcon.objects.layerSet import LayerSet
13from defcon.objects.layer import Layer
14from defcon.objects.info import Info
15from defcon.objects.kerning import Kerning
16from defcon.objects.groups import Groups
17from defcon.objects.features import Features
18from defcon.objects.lib import Lib
19from defcon.objects.imageSet import ImageSet
20from defcon.objects.dataSet import DataSet
21from defcon.tools.notifications import NotificationCenter
22
23
24class 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
1319def _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
1328def _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
1346def _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
1363def _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
1377def _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
1442def _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
1454def _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
1468def _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
1497def _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
1521def _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
1534def _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
1545def _testBounds():
1546    """
1547    >>> from defcon.test.testTools import getTestFontPath
1548    >>> font = Font(getTestFontPath())
1549    >>> font.bounds
1550    (0, 0, 700, 700)
1551    """
1552
1553def _testControlPointBounds():
1554    """
1555    >>> from defcon.test.testTools import getTestFontPath
1556    >>> font = Font(getTestFontPath())
1557    >>> font.controlPointBounds
1558    (0, 0, 700, 700)
1559    """
1560
1561def _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
1592def _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
1606def _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
1628def _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
1677def _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
1705def _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
1733def _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
1761def _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
1789def _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
1821def _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
1842if __name__ == "__main__":
1843    import doctest
1844    doctest.testmod()
Note: See TracBrowser for help on using the repository browser.