1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2016 Georg Seifert. All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18from __future__ import print_function, division, unicode_literals
19
20import re
21import math
22import inspect
23import uuid
24import logging
25import glyphsLib
26from glyphsLib.types import (
27    ValueType,
28    Transform,
29    Point,
30    Rect,
31    parse_datetime,
32    parse_color,
33    floatToString,
34    readIntlist,
35    UnicodesList,
36)
37from glyphsLib.parser import Parser
38from glyphsLib.writer import Writer
39from collections import OrderedDict
40from fontTools.misc.py23 import unicode, basestring, UnicodeIO, unichr, open
41from glyphsLib.affine import Affine
42
43
44logger = logging.getLogger(__name__)
45
46__all__ = [
47    "Glyphs",
48    "GSFont",
49    "GSFontMaster",
50    "GSAlignmentZone",
51    "GSInstance",
52    "GSCustomParameter",
53    "GSClass",
54    "GSFeaturePrefix",
55    "GSFeature",
56    "GSGlyph",
57    "GSLayer",
58    "GSAnchor",
59    "GSComponent",
60    "GSSmartComponentAxis",
61    "GSPath",
62    "GSNode",
63    "GSGuideLine",
64    "GSAnnotation",
65    "GSHint",
66    "GSBackgroundImage",
67    # Constants
68    "__all__",
69    "MOVE",
70    "LINE",
71    "CURVE",
72    "OFFCURVE",
73    "GSMOVE",
74    "GSLINE",
75    "GSCURVE",
76    "GSOFFCURVE",
77    "GSSHARP",
78    "GSSMOOTH",
79    "TAG",
80    "TOPGHOST",
81    "STEM",
82    "BOTTOMGHOST",
83    "TTANCHOR",
84    "TTSTEM",
85    "TTALIGN",
86    "TTINTERPOLATE",
87    "TTDIAGONAL",
88    "TTDELTA",
89    "CORNER",
90    "CAP",
91    "TTDONTROUND",
92    "TTROUND",
93    "TTROUNDUP",
94    "TTROUNDDOWN",
95    "TRIPLE",
96    "TEXT",
97    "ARROW",
98    "CIRCLE",
99    "PLUS",
100    "MINUS",
101    "LTR",
102    "RTL",
103    "LTRTTB",
104    "RTLTTB",
105    "GSTopLeft",
106    "GSTopCenter",
107    "GSTopRight",
108    "GSCenterLeft",
109    "GSCenterCenter",
110    "GSCenterRight",
111    "GSBottomLeft",
112    "GSBottomCenter",
113    "GSBottomRight",
114    "WEIGHT_CODES",
115    "WIDTH_CODES",
116]
117
118# CONSTANTS
119GSMOVE_ = 17
120GSLINE_ = 1
121GSCURVE_ = 35
122GSOFFCURVE_ = 65
123GSSHARP = 0
124GSSMOOTH = 100
125
126GSMOVE = "move"
127GSLINE = "line"
128GSCURVE = "curve"
129GSQCURVE = "qcurve"
130GSOFFCURVE = "offcurve"
131
132MOVE = "move"
133LINE = "line"
134CURVE = "curve"
135OFFCURVE = "offcurve"
136
137TAG = -2
138TOPGHOST = -1
139STEM = 0
140BOTTOMGHOST = 1
141TTANCHOR = 2
142TTSTEM = 3
143TTALIGN = 4
144TTINTERPOLATE = 5
145TTDIAGONAL = 6
146TTDELTA = 7
147CORNER = 16
148CAP = 17
149
150TTDONTROUND = 4
151TTROUND = 0
152TTROUNDUP = 1
153TTROUNDDOWN = 2
154TRIPLE = 128
155
156# Annotations:
157TEXT = 1
158ARROW = 2
159CIRCLE = 3
160PLUS = 4
161MINUS = 5
162
163# Directions:
164LTR = 0  # Left To Right (e.g. Latin)
165RTL = 1  # Right To Left (e.g. Arabic, Hebrew)
166LTRTTB = 3  # Left To Right, Top To Bottom
167RTLTTB = 2  # Right To Left, Top To Bottom
168
169# Reverse lookup for __repr__
170hintConstants = {
171    -2: "Tag",
172    -1: "TopGhost",
173    0: "Stem",
174    1: "BottomGhost",
175    2: "TTAnchor",
176    3: "TTStem",
177    4: "TTAlign",
178    5: "TTInterpolate",
179    6: "TTDiagonal",
180    7: "TTDelta",
181    16: "Corner",
182    17: "Cap",
183}
184
185GSTopLeft = 6
186GSTopCenter = 7
187GSTopRight = 8
188GSCenterLeft = 3
189GSCenterCenter = 4
190GSCenterRight = 5
191GSBottomLeft = 0
192GSBottomCenter = 1
193GSBottomRight = 2
194
195# Writing direction
196LTR = 0
197RTL = 1
198LTRTTB = 3
199RTLTTB = 2
200
201
202WEIGHT_CODES = {
203    "Thin": 100,
204    "ExtraLight": 200,
205    "UltraLight": 200,
206    "Light": 300,
207    None: 400,  # default value normally omitted in source
208    "Normal": 400,
209    "Regular": 400,
210    "Medium": 500,
211    "DemiBold": 600,
212    "SemiBold": 600,
213    "Bold": 700,
214    "UltraBold": 800,
215    "ExtraBold": 800,
216    "Black": 900,
217    "Heavy": 900,
218}
219
220WIDTH_CODES = {
221    "Ultra Condensed": 1,
222    "Extra Condensed": 2,
223    "Condensed": 3,
224    "SemiCondensed": 4,
225    None: 5,  # default value normally omitted in source
226    "Medium (normal)": 5,
227    "Semi Expanded": 6,
228    "Expanded": 7,
229    "Extra Expanded": 8,
230    "Ultra Expanded": 9,
231}
232
233
234class OnlyInGlyphsAppError(NotImplementedError):
235    def __init__(self):
236        NotImplementedError.__init__(
237            self,
238            "This property/method is only available in the real UI-based "
239            "version of Glyphs.app.",
240        )
241
242
243def parse_hint_target(line=None):
244    if line is None:
245        return None
246    if line[0] == "{":
247        return Point(line)
248    else:
249        return line
250
251
252def isString(string):
253    return isinstance(string, (str, unicode))
254
255
256def transformStructToScaleAndRotation(transform):
257    Det = transform[0] * transform[3] - transform[1] * transform[2]
258    _sX = math.sqrt(math.pow(transform[0], 2) + math.pow(transform[1], 2))
259    _sY = math.sqrt(math.pow(transform[2], 2) + math.pow(transform[3], 2))
260    if Det < 0:
261        _sY = -_sY
262    _R = math.atan2(transform[1] * _sY, transform[0] * _sX) * 180 / math.pi
263
264    if Det < 0 and (math.fabs(_R) > 135 or _R < -90):
265        _sX = -_sX
266        _sY = -_sY
267        if _R < 0:
268            _R += 180
269        else:
270            _R -= 180
271
272    quadrant = 0
273    if _R < -90:
274        quadrant = 180
275        _R += quadrant
276    if _R > 90:
277        quadrant = -180
278        _R += quadrant
279    _R = _R * _sX / _sY
280    _R -= quadrant
281    if _R < -179:
282        _R += 360
283
284    return _sX, _sY, _R
285
286
287class GSApplication(object):
288    def __init__(self):
289        self.font = None
290        self.fonts = []
291
292    def open(self, path):
293        newFont = GSFont(path)
294        self.fonts.append(newFont)
295        self.font = newFont
296        return newFont
297
298    def __repr__(self):
299        return "<glyphsLib>"
300
301
302Glyphs = GSApplication()
303
304
305class GSBase(object):
306    _classesForName = {}
307    _defaultsForName = {}
308    _wrapperKeysTranslate = {}
309
310    def __init__(self):
311        for key in self._classesForName.keys():
312            if not hasattr(self, key):
313                klass = self._classesForName[key]
314                if inspect.isclass(klass) and issubclass(klass, GSBase):
315                    # FIXME: (jany) Why?
316                    # For GSLayer::backgroundImage, I was getting []
317                    # instead of None when no image
318                    value = []
319                elif key in self._defaultsForName:
320                    value = self._defaultsForName.get(key)
321                else:
322                    value = klass()
323                key = self._wrapperKeysTranslate.get(key, key)
324                setattr(self, key, value)
325
326    def __repr__(self):
327        content = ""
328        if hasattr(self, "_dict"):
329            content = str(self._dict)
330        return "<{} {}>".format(self.__class__.__name__, content)
331
332    def classForName(self, name):
333        return self._classesForName.get(name, str)
334
335    def default_attr_value(self, attr_name):
336        """Return the default value of the given attribute, if any."""
337
338    # Note:
339    # The dictionary API exposed by GS* classes is "private" in the sense that:
340    #  * it should only be used by the parser, so it should only
341    #    work for key names that are found in the files
342    #  * and only for filling data in the objects, which is why it only
343    #    implements `__setitem__`
344    #
345    # Users of the library should only rely on the object-oriented API that is
346    # documented at https://docu.glyphsapp.com/
347    def __setitem__(self, key, value):
348        if isinstance(value, bytes) and key in self._classesForName:
349            new_type = self._classesForName[key]
350            if new_type is unicode:
351                value = value.decode("utf-8")
352            else:
353                try:
354                    value = new_type().read(value)
355                except Exception:
356                    # FIXME: too broad, should only catch specific exceptions
357                    value = new_type(value)
358        key = self._wrapperKeysTranslate.get(key, key)
359        setattr(self, key, value)
360
361    def shouldWriteValueForKey(self, key):
362        getKey = self._wrapperKeysTranslate.get(key, key)
363        value = getattr(self, getKey)
364        klass = self._classesForName[key]
365        default = self._defaultsForName.get(key, None)
366        if (
367            isinstance(value, (list, glyphsLib.classes.Proxy, str, unicode))
368            and len(value) == 0
369        ):
370            return False
371        if default is not None:
372            return default != value
373        if klass in (int, float, bool) and value == 0:
374            return False
375        if isinstance(value, ValueType) and value.value is None:
376            return False
377        return True
378
379
380class Proxy(object):
381    def __init__(self, owner):
382        self._owner = owner
383
384    def __repr__(self):
385        """Return list-lookalike of representation string of objects"""
386        strings = []
387        for currItem in self:
388            strings.append("%s" % currItem)
389        return "(%s)" % (", ".join(strings))
390
391    def __len__(self):
392        values = self.values()
393        if values is not None:
394            return len(values)
395        return 0
396
397    def pop(self, i):
398        if type(i) == int:
399            node = self[i]
400            del self[i]
401            return node
402        else:
403            raise KeyError
404
405    def __iter__(self):
406        values = self.values()
407        if values is not None:
408            for element in values:
409                yield element
410
411    def index(self, value):
412        return self.values().index(value)
413
414    def __copy__(self):
415        return list(self)
416
417    def __deepcopy__(self, memo):
418        return [x.copy() for x in self.values()]
419
420    def setter(self, values):
421        method = self.setterMethod()
422        if type(values) == list:
423            method(values)
424        elif (
425            type(values) == tuple
426            or values.__class__.__name__ == "__NSArrayM"
427            or type(values) == type(self)
428        ):
429            method(list(values))
430        elif values is None:
431            method(list())
432        else:
433            raise TypeError
434
435
436class LayersIterator:
437    def __init__(self, owner):
438        self.curInd = 0
439        self._owner = owner
440        self._orderedLayers = None
441
442    def __iter__(self):
443        return self
444
445    def next(self):
446        return self.__next__()
447
448    def __next__(self):
449        if self._owner.parent:
450            if self.curInd >= len(self._owner.layers):
451                raise StopIteration
452            item = self.orderedLayers[self.curInd]
453        else:
454            if self.curInd >= len(self._owner._layers):
455                raise StopIteration
456            item = self._owner._layers[self.curInd]
457        self.curInd += 1
458        return item
459
460    @property
461    def orderedLayers(self):
462        if not self._orderedLayers:
463            glyphLayerIds = [
464                l.associatedMasterId
465                for l in self._owner._layers.values()
466                if l.associatedMasterId == l.layerId
467            ]
468            masterIds = [m.id for m in self._owner.parent.masters]
469            intersectedLayerIds = set(glyphLayerIds) & set(masterIds)
470            orderedLayers = [
471                self._owner._layers[m.id]
472                for m in self._owner.parent.masters
473                if m.id in intersectedLayerIds
474            ]
475            orderedLayers += [
476                self._owner._layers[l.layerId]
477                for l in self._owner._layers.values()
478                if l.layerId not in intersectedLayerIds
479            ]
480            self._orderedLayers = orderedLayers
481        return self._orderedLayers
482
483
484class FontFontMasterProxy(Proxy):
485    """The list of masters. You can access it with the index or the master ID.
486    Usage:
487        Font.masters[index]
488        Font.masters[id]
489        for master in Font.masters:
490        ...
491    """
492
493    def __getitem__(self, Key):
494        if type(Key) == slice:
495            return self.values().__getitem__(Key)
496        if type(Key) is int:
497            if Key < 0:
498                Key = self.__len__() + Key
499            return self.values()[Key]
500        elif isString(Key):
501            for master in self.values():
502                if master.id == Key:
503                    return master
504        else:
505            raise KeyError
506
507    def __setitem__(self, Key, FontMaster):
508        FontMaster.font = self._owner
509        if type(Key) is int:
510            OldFontMaster = self.__getitem__(Key)
511            if Key < 0:
512                Key = self.__len__() + Key
513            FontMaster.id = OldFontMaster.id
514            self._owner._masters[Key] = FontMaster
515        elif isString(Key):
516            OldFontMaster = self.__getitem__(Key)
517            FontMaster.id = OldFontMaster.id
518            Index = self._owner._masters.index(OldFontMaster)
519            self._owner._masters[Index] = FontMaster
520        else:
521            raise KeyError
522
523    def __delitem__(self, Key):
524        if type(Key) is int:
525            if Key < 0:
526                Key = self.__len__() + Key
527            return self.remove(self._owner._masters[Key])
528        else:
529            OldFontMaster = self.__getitem__(Key)
530            return self.remove(OldFontMaster)
531
532    def values(self):
533        return self._owner._masters
534
535    def append(self, FontMaster):
536        FontMaster.font = self._owner
537        if not FontMaster.id:
538            FontMaster.id = str(uuid.uuid4()).upper()
539        self._owner._masters.append(FontMaster)
540
541        # Cycle through all glyphs and append layer
542        for glyph in self._owner.glyphs:
543            if not glyph.layers[FontMaster.id]:
544                newLayer = GSLayer()
545                glyph._setupLayer(newLayer, FontMaster.id)
546                glyph.layers.append(newLayer)
547
548    def remove(self, FontMaster):
549
550        # First remove all layers in all glyphs that reference this master
551        for glyph in self._owner.glyphs:
552            for layer in glyph.layers:
553                if (
554                    layer.associatedMasterId == FontMaster.id
555                    or layer.layerId == FontMaster.id
556                ):
557                    glyph.layers.remove(layer)
558
559        self._owner._masters.remove(FontMaster)
560
561    def insert(self, Index, FontMaster):
562        FontMaster.font = self._owner
563        self._owner._masters.insert(Index, FontMaster)
564
565    def extend(self, FontMasters):
566        for FontMaster in FontMasters:
567            self.append(FontMaster)
568
569    def setter(self, values):
570        if isinstance(values, Proxy):
571            values = list(values)
572        self._owner._masters = values
573        for m in self._owner._masters:
574            m.font = self._owner
575
576
577class FontGlyphsProxy(Proxy):
578    """The list of glyphs. You can access it with the index or the glyph name.
579    Usage:
580        Font.glyphs[index]
581        Font.glyphs[name]
582        for glyph in Font.glyphs:
583        ...
584    """
585
586    def __getitem__(self, key):
587        if type(key) == slice:
588            return self.values().__getitem__(key)
589
590        # by index
591        if isinstance(key, int):
592            return self._owner._glyphs[key]
593
594        if isinstance(key, basestring):
595            return self._get_glyph_by_string(key)
596
597        return None
598
599    def __setitem__(self, key, glyph):
600        if type(key) is int:
601            self._owner._setupGlyph(glyph)
602            self._owner._glyphs[key] = glyph
603        else:
604            raise KeyError  # TODO: add other access methods
605
606    def __delitem__(self, key):
607        if type(key) is int:
608            del (self._owner._glyph[key])
609        else:
610            raise KeyError  # TODO: add other access methods
611
612    def __contains__(self, item):
613        if isString(item):
614            return self._get_glyph_by_string(item) is not None
615        return item in self._owner._glyphs
616
617    def _get_glyph_by_string(self, key):
618        # FIXME: (jany) looks inefficient
619        if isinstance(key, basestring):
620            # by glyph name
621            for glyph in self._owner._glyphs:
622                if glyph.name == key:
623                    return glyph
624            # by string representation as u'ä'
625            if len(key) == 1:
626                for glyph in self._owner._glyphs:
627                    if glyph.unicode == "%04X" % (ord(key)):
628                        return glyph
629            # by unicode
630            else:
631                for glyph in self._owner._glyphs:
632                    if glyph.unicode == key.upper():
633                        return glyph
634        return None
635
636    def values(self):
637        return self._owner._glyphs
638
639    def items(self):
640        items = []
641        for value in self._owner._glyphs:
642            key = value.name
643            items.append((key, value))
644        return items
645
646    def append(self, glyph):
647        self._owner._setupGlyph(glyph)
648        self._owner._glyphs.append(glyph)
649
650    def extend(self, objects):
651        for glyph in objects:
652            self._owner._setupGlyph(glyph)
653        self._owner._glyphs.extend(list(objects))
654
655    def __len__(self):
656        return len(self._owner._glyphs)
657
658    def setter(self, values):
659        if isinstance(values, Proxy):
660            values = list(values)
661        self._owner._glyphs = values
662        for g in self._owner._glyphs:
663            g.parent = self._owner
664            for layer in g.layers.values():
665                if (
666                    not hasattr(layer, "associatedMasterId")
667                    or layer.associatedMasterId is None
668                    or len(layer.associatedMasterId) == 0
669                ):
670                    g._setupLayer(layer, layer.layerId)
671
672
673class FontClassesProxy(Proxy):
674    def __getitem__(self, key):
675        if isinstance(key, (slice, int)):
676            return self.values().__getitem__(key)
677        if isinstance(key, (str, unicode)):
678            for index, klass in enumerate(self.values()):
679                if klass.name == key:
680                    return self.values()[index]
681        raise KeyError
682
683    def __setitem__(self, key, value):
684        if isinstance(key, int):
685            self.values()[key] = value
686            value._parent = self._owner
687        elif isinstance(key, (str, unicode)):
688            for index, klass in enumerate(self.values()):
689                if klass.name == key:
690                    self.values()[index] = value
691                    value._parent = self._owner
692        else:
693            raise KeyError
694
695    def __delitem__(self, key):
696        if isinstance(key, int):
697            del self.values()[key]
698        elif isinstance(key, (str, unicode)):
699            for index, klass in enumerate(self.values()):
700                if klass.name == key:
701                    del self.values()[index]
702
703    # FIXME: (jany) def __contains__
704
705    def append(self, item):
706        self.values().append(item)
707        item._parent = self._owner
708
709    def insert(self, key, item):
710        self.values().insert(key, item)
711        item._parent = self._owner
712
713    def extend(self, items):
714        self.values().extend(items)
715        for value in items:
716            value._parent = self._owner
717
718    def remove(self, item):
719        self.values().remove(item)
720
721    def values(self):
722        return self._owner._classes
723
724    def setter(self, values):
725        if isinstance(values, Proxy):
726            values = list(values)
727        self._owner._classes = values
728        for value in values:
729            value._parent = self._owner
730
731
732class GlyphLayerProxy(Proxy):
733    def __getitem__(self, key):
734        self._ensureMasterLayers()
735        if isinstance(key, slice):
736            return self.values().__getitem__(key)
737        elif isinstance(key, int):
738            if self._owner.parent:
739                return list(self)[key]
740            return list(self.values())[key]
741        elif isString(key):
742            if key in self._owner._layers:
743                return self._owner._layers[key]
744
745    def __setitem__(self, key, layer):
746        if isinstance(key, int) and self._owner.parent:
747            OldLayer = self._owner._layers.values()[key]
748            if key < 0:
749                key = self.__len__() + key
750            layer.layerId = OldLayer.layerId
751            layer.associatedMasterId = OldLayer.associatedMasterId
752            self._owner._setupLayer(layer, OldLayer.layerId)
753            self._owner._layers[key] = layer
754        elif isinstance(key, basestring) and self._owner.parent:
755            # FIXME: (jany) more work to do?
756            layer.parent = self._owner
757            self._owner._layers[key] = layer
758        else:
759            raise KeyError
760
761    def __delitem__(self, key):
762        if isinstance(key, int) and self._owner.parent:
763            if key < 0:
764                key = self.__len__() + key
765            Layer = self.__getitem__(key)
766            key = Layer.layerId
767        del (self._owner._layers[key])
768
769    def __iter__(self):
770        return LayersIterator(self._owner)
771
772    def __len__(self):
773        return len(self.values())
774
775    def keys(self):
776        self._ensureMasterLayers()
777        return self._owner._layers.keys()
778
779    def values(self):
780        self._ensureMasterLayers()
781        return self._owner._layers.values()
782
783    def append(self, layer):
784        assert layer is not None
785        self._ensureMasterLayers()
786        if not layer.associatedMasterId:
787            if self._owner.parent:
788                layer.associatedMasterId = self._owner.parent.masters[0].id
789        if not layer.layerId:
790            layer.layerId = str(uuid.uuid4()).upper()
791        self._owner._setupLayer(layer, layer.layerId)
792        self._owner._layers[layer.layerId] = layer
793
794    def extend(self, layers):
795        for layer in layers:
796            self.append(layer)
797
798    def remove(self, layer):
799        return self._owner.removeLayerForKey_(layer.layerId)
800
801    def insert(self, index, layer):
802        self._ensureMasterLayers()
803        self.append(layer)
804
805    def setter(self, values):
806        newLayers = OrderedDict()
807        if type(values) == list or type(values) == tuple or type(values) == type(self):
808            for layer in values:
809                newLayers[layer.layerId] = layer
810        elif type(values) == dict:  # or isinstance(values, NSDictionary)
811            for layer in values.values():
812                newLayers[layer.layerId] = layer
813        else:
814            raise TypeError
815        for (key, layer) in newLayers.items():
816            self._owner._setupLayer(layer, key)
817        self._owner._layers = newLayers
818
819    def _ensureMasterLayers(self):
820        # Ensure existence of master-linked layers (even for iteration, len() etc.)
821        # if accidentally deleted
822        if not self._owner.parent:
823            return
824        for master in self._owner.parent.masters:
825            # if (master.id not in self._owner._layers or
826            #         self._owner._layers[master.id] is None):
827            if self._owner.parent.masters[master.id] is None:
828                newLayer = GSLayer()
829                newLayer.associatedMasterId = master.id
830                newLayer.layerId = master.id
831                self._owner._setupLayer(newLayer, master.id)
832                self.__setitem__(master.id, newLayer)
833
834    def plistArray(self):
835        return list(self._owner._layers.values())
836
837
838class LayerAnchorsProxy(Proxy):
839    def __getitem__(self, key):
840        if isinstance(key, (slice, int)):
841            return self.values().__getitem__(key)
842        elif isinstance(key, (str, unicode)):
843            for i, a in enumerate(self._owner._anchors):
844                if a.name == key:
845                    return self._owner._anchors[i]
846        else:
847            raise KeyError
848
849    def __setitem__(self, key, anchor):
850        if isinstance(key, (str, unicode)):
851            anchor.name = key
852            for i, a in enumerate(self._owner._anchors):
853                if a.name == key:
854                    self._owner._anchors[i] = anchor
855                    return
856            anchor._parent = self._owner
857            self._owner._anchors.append(anchor)
858        else:
859            raise TypeError
860
861    def __delitem__(self, key):
862        if isinstance(key, int):
863            del self._owner._anchors[key]
864        elif isinstance(key, (str, unicode)):
865            for i, a in enumerate(self._owner._anchors):
866                if a.name == key:
867                    self._owner._anchors[i]._parent = None
868                    del self._owner._anchors[i]
869                    return
870
871    def values(self):
872        return self._owner._anchors
873
874    def append(self, anchor):
875        for i, a in enumerate(self._owner._anchors):
876            if a.name == anchor.name:
877                anchor._parent = self._owner
878                self._owner._anchors[i] = anchor
879                return
880        if anchor.name:
881            self._owner._anchors.append(anchor)
882        else:
883            raise ValueError("Anchor must have name")
884
885    def extend(self, anchors):
886        for anchor in anchors:
887            anchor._parent = self._owner
888        self._owner._anchors.extend(anchors)
889
890    def remove(self, anchor):
891        if isinstance(anchor, (str, unicode)):
892            anchor = self.values()[anchor]
893        return self._owner._anchors.remove(anchor)
894
895    def insert(self, index, anchor):
896        anchor._parent = self._owner
897        self._owner._anchors.insert(index, anchor)
898
899    def __len__(self):
900        return len(self._owner._anchors)
901
902    def setter(self, anchors):
903        if isinstance(anchors, Proxy):
904            anchors = list(anchors)
905        self._owner._anchors = anchors
906        for anchor in anchors:
907            anchor._parent = self._owner
908
909
910class IndexedObjectsProxy(Proxy):
911    def __getitem__(self, key):
912        if isinstance(key, (slice, int)):
913            return self.values().__getitem__(key)
914        else:
915            raise KeyError
916
917    def __setitem__(self, key, value):
918        if isinstance(key, int):
919            self.values()[key] = value
920            value._parent = self._owner
921        else:
922            raise KeyError
923
924    def __delitem__(self, key):
925        if isinstance(key, int):
926            del self.values()[key]
927        else:
928            raise KeyError
929
930    def values(self):
931        return getattr(self._owner, self._objects_name)
932
933    def append(self, value):
934        self.values().append(value)
935        value._parent = self._owner
936
937    def extend(self, values):
938        self.values().extend(values)
939        for value in values:
940            value._parent = self._owner
941
942    def remove(self, value):
943        self.values().remove(value)
944
945    def insert(self, index, value):
946        self.values().insert(index, value)
947        value._parent = self._owner
948
949    def __len__(self):
950        return len(self.values())
951
952    def setter(self, values):
953        setattr(self._owner, self._objects_name, list(values))
954        for value in self.values():
955            value._parent = self._owner
956
957
958class LayerPathsProxy(IndexedObjectsProxy):
959    _objects_name = "_paths"
960
961    def __init__(self, owner):
962        super(LayerPathsProxy, self).__init__(owner)
963
964
965class LayerHintsProxy(IndexedObjectsProxy):
966    _objects_name = "_hints"
967
968    def __init__(self, owner):
969        super(LayerHintsProxy, self).__init__(owner)
970
971
972class LayerComponentsProxy(IndexedObjectsProxy):
973    _objects_name = "_components"
974
975    def __init__(self, owner):
976        super(LayerComponentsProxy, self).__init__(owner)
977
978
979class LayerAnnotationProxy(IndexedObjectsProxy):
980    _objects_name = "_annotations"
981
982    def __init__(self, owner):
983        super(LayerAnnotationProxy, self).__init__(owner)
984
985
986class LayerGuideLinesProxy(IndexedObjectsProxy):
987    _objects_name = "_guides"
988
989    def __init__(self, owner):
990        super(LayerGuideLinesProxy, self).__init__(owner)
991
992
993class PathNodesProxy(IndexedObjectsProxy):
994    _objects_name = "_nodes"
995
996    def __init__(self, owner):
997        super(PathNodesProxy, self).__init__(owner)
998
999
1000class CustomParametersProxy(Proxy):
1001    def __getitem__(self, key):
1002        if isinstance(key, slice):
1003            return self.values().__getitem__(key)
1004        if isinstance(key, int):
1005            return self._owner._customParameters[key]
1006        else:
1007            customParameter = self._get_parameter_by_key(key)
1008            if customParameter is not None:
1009                return customParameter.value
1010        return None
1011
1012    def _get_parameter_by_key(self, key):
1013        for customParameter in self._owner._customParameters:
1014            if customParameter.name == key:
1015                return customParameter
1016
1017    def __setitem__(self, key, value):
1018        customParameter = self._get_parameter_by_key(key)
1019        if customParameter is not None:
1020            customParameter.value = value
1021        else:
1022            parameter = GSCustomParameter(name=key, value=value)
1023            self._owner._customParameters.append(parameter)
1024
1025    def __delitem__(self, key):
1026        if isinstance(key, int):
1027            del self._owner._customParameters[key]
1028        elif isinstance(key, basestring):
1029            for parameter in self._owner._customParameters:
1030                if parameter.name == key:
1031                    self._owner._customParameters.remove(parameter)
1032        else:
1033            raise KeyError
1034
1035    def __contains__(self, item):
1036        if isString(item):
1037            return self.__getitem__(item) is not None
1038        return item in self._owner._customParameters
1039
1040    def __iter__(self):
1041        for index in range(len(self._owner._customParameters)):
1042            yield self._owner._customParameters[index]
1043
1044    def append(self, parameter):
1045        parameter.parent = self._owner
1046        self._owner._customParameters.append(parameter)
1047
1048    def extend(self, parameters):
1049        for parameter in parameters:
1050            parameter.parent = self._owner
1051        self._owner._customParameters.extend(parameters)
1052
1053    def remove(self, parameter):
1054        if isString(parameter):
1055            parameter = self.__getitem__(parameter)
1056        self._owner._customParameters.remove(parameter)
1057
1058    def insert(self, index, parameter):
1059        parameter.parent = self._owner
1060        self._owner._customParameters.insert(index, parameter)
1061
1062    def __len__(self):
1063        return len(self._owner._customParameters)
1064
1065    def values(self):
1066        return self._owner._customParameters
1067
1068    def __setter__(self, parameters):
1069        for parameter in parameters:
1070            parameter.parent = self._owner
1071        self._owner._customParameters = parameters
1072
1073    def setterMethod(self):
1074        return self.__setter__
1075
1076
1077class UserDataProxy(Proxy):
1078    def __getitem__(self, key):
1079        if self._owner._userData is None:
1080            return None
1081        # This is not the normal `dict` behaviour, because this does not raise
1082        # `KeyError` and instead just returns `None`. It matches Glyphs.app.
1083        return self._owner._userData.get(key)
1084
1085    def __setitem__(self, key, value):
1086        if self._owner._userData is not None:
1087            self._owner._userData[key] = value
1088        else:
1089            self._owner._userData = {key: value}
1090
1091    def __delitem__(self, key):
1092        if self._owner._userData is not None and key in self._owner._userData:
1093            del self._owner._userData[key]
1094
1095    def __contains__(self, item):
1096        if self._owner._userData is None:
1097            return False
1098        return item in self._owner._userData
1099
1100    def __iter__(self):
1101        if self._owner._userData is None:
1102            return
1103        # This is not the normal `dict` behaviour, because this yields values
1104        # instead of keys. It matches Glyphs.app though. Urg.
1105        for value in self._owner._userData.values():
1106            yield value
1107
1108    def values(self):
1109        if self._owner._userData is None:
1110            return []
1111        return self._owner._userData.values()
1112
1113    def keys(self):
1114        if self._owner._userData is None:
1115            return []
1116        return self._owner._userData.keys()
1117
1118    def get(self, key):
1119        if self._owner._userData is None:
1120            return None
1121        return self._owner._userData.get(key)
1122
1123    def setter(self, values):
1124        self._owner._userData = values
1125
1126
1127class GSCustomParameter(GSBase):
1128    _classesForName = {"name": unicode, "value": None}
1129
1130    _CUSTOM_INT_PARAMS = frozenset(
1131        (
1132            "ascender",
1133            "blueShift",
1134            "capHeight",
1135            "descender",
1136            "hheaAscender",
1137            "hheaDescender",
1138            "hheaLineGap",
1139            "macintoshFONDFamilyID",
1140            "openTypeHeadLowestRecPPEM",
1141            "openTypeHheaAscender",
1142            "openTypeHheaCaretOffset",
1143            "openTypeHheaCaretSlopeRise",
1144            "openTypeHheaCaretSlopeRun",
1145            "openTypeHheaDescender",
1146            "openTypeHheaLineGap",
1147            "openTypeOS2StrikeoutPosition",
1148            "openTypeOS2StrikeoutSize",
1149            "openTypeOS2SubscriptXOffset",
1150            "openTypeOS2SubscriptXSize",
1151            "openTypeOS2SubscriptYOffset",
1152            "openTypeOS2SubscriptYSize",
1153            "openTypeOS2SuperscriptXOffset",
1154            "openTypeOS2SuperscriptXSize",
1155            "openTypeOS2SuperscriptYOffset",
1156            "openTypeOS2SuperscriptYSize",
1157            "openTypeOS2TypoAscender",
1158            "openTypeOS2TypoDescender",
1159            "openTypeOS2TypoLineGap",
1160            "openTypeOS2WeightClass",
1161            "openTypeOS2WidthClass",
1162            "openTypeOS2WinAscent",
1163            "openTypeOS2WinDescent",
1164            "openTypeVheaCaretOffset",
1165            "openTypeVheaCaretSlopeRise",
1166            "openTypeVheaCaretSlopeRun",
1167            "openTypeVheaVertTypoAscender",
1168            "openTypeVheaVertTypoDescender",
1169            "openTypeVheaVertTypoLineGap",
1170            "postscriptBlueFuzz",
1171            "postscriptBlueShift",
1172            "postscriptDefaultWidthX",
1173            "postscriptUnderlinePosition",
1174            "postscriptUnderlineThickness",
1175            "postscriptUniqueID",
1176            "postscriptWindowsCharacterSet",
1177            "shoulderHeight",
1178            "smallCapHeight",
1179            "typoAscender",
1180            "typoDescender",
1181            "typoLineGap",
1182            "underlinePosition",
1183            "underlineThickness",
1184            "unitsPerEm",
1185            "vheaVertAscender",
1186            "vheaVertDescender",
1187            "vheaVertLineGap",
1188            "weightClass",
1189            "widthClass",
1190            "winAscent",
1191            "winDescent",
1192            "year",
1193            "Grid Spacing",
1194        )
1195    )
1196    _CUSTOM_FLOAT_PARAMS = frozenset(("postscriptSlantAngle", "postscriptBlueScale"))
1197
1198    _CUSTOM_BOOL_PARAMS = frozenset(
1199        (
1200            "isFixedPitch",
1201            "postscriptForceBold",
1202            "postscriptIsFixedPitch",
1203            "Don\u2019t use Production Names",
1204            "DisableAllAutomaticBehaviour",
1205            "Use Typo Metrics",
1206            "Has WWS Names",
1207            "Use Extension Kerning",
1208            "Disable Subroutines",
1209            "Don't use Production Names",
1210            "Disable Last Change",
1211        )
1212    )
1213    _CUSTOM_INTLIST_PARAMS = frozenset(
1214        (
1215            "fsType",
1216            "openTypeOS2CodePageRanges",
1217            "openTypeOS2FamilyClass",
1218            "openTypeOS2Panose",
1219            "openTypeOS2Type",
1220            "openTypeOS2UnicodeRanges",
1221            "panose",
1222            "unicodeRanges",
1223            "codePageRanges",
1224            "openTypeHeadFlags",
1225        )
1226    )
1227    _CUSTOM_DICT_PARAMS = frozenset("GASP Table")
1228
1229    def __init__(self, name="New Value", value="New Parameter"):
1230        self.name = name
1231        self.value = value
1232
1233    def __repr__(self):
1234        return "<{} {}: {}>".format(self.__class__.__name__, self.name, self._value)
1235
1236    def plistValue(self):
1237        string = UnicodeIO()
1238        writer = Writer(string)
1239        writer.writeDict({"name": self.name, "value": self.value})
1240        return string.getvalue()
1241
1242    def getValue(self):
1243        return self._value
1244
1245    def setValue(self, value):
1246        """Cast some known data in custom parameters."""
1247        if self.name in self._CUSTOM_INT_PARAMS:
1248            value = int(value)
1249        elif self.name in self._CUSTOM_FLOAT_PARAMS:
1250            value = float(value)
1251        elif self.name in self._CUSTOM_BOOL_PARAMS:
1252            value = bool(value)
1253        elif self.name in self._CUSTOM_INTLIST_PARAMS:
1254            value = readIntlist(value)
1255        elif self.name in self._CUSTOM_DICT_PARAMS:
1256            parser = Parser()
1257            value = parser.parse(value)
1258        elif self.name == "note":
1259            value = unicode(value)
1260        self._value = value
1261
1262    value = property(getValue, setValue)
1263
1264
1265class GSAlignmentZone(GSBase):
1266    def __init__(self, pos=0, size=20):
1267        super(GSAlignmentZone, self).__init__()
1268        self.position = pos
1269        self.size = size
1270
1271    def read(self, src):
1272        if src is not None:
1273            p = Point(src)
1274            self.position = float(p.value[0])
1275            self.size = float(p.value[1])
1276        return self
1277
1278    def __repr__(self):
1279        return "<{} pos:{:g} size:{:g}>".format(
1280            self.__class__.__name__, self.position, self.size
1281        )
1282
1283    def __lt__(self, other):
1284        return (self.position, self.size) < (other.position, other.size)
1285
1286    def plistValue(self):
1287        return '"{{{}, {}}}"'.format(
1288            floatToString(self.position), floatToString(self.size)
1289        )
1290
1291
1292class GSGuideLine(GSBase):
1293    _classesForName = {
1294        "alignment": str,
1295        "angle": float,
1296        "locked": bool,
1297        "position": Point,
1298        "showMeasurement": bool,
1299        "filter": str,
1300        "name": unicode,
1301    }
1302    _parent = None
1303    _defaultsForName = {"position": Point(0, 0)}
1304
1305    def __init__(self):
1306        super(GSGuideLine, self).__init__()
1307
1308    def __repr__(self):
1309        return "<{} x={:.1f} y={:.1f} angle={:.1f}>".format(
1310            self.__class__.__name__, self.position.x, self.position.y, self.angle
1311        )
1312
1313    @property
1314    def parent(self):
1315        return self._parent
1316
1317
1318MASTER_NAME_WEIGHTS = ("Light", "SemiLight", "SemiBold", "Bold")
1319MASTER_NAME_WIDTHS = ("Condensed", "SemiCondensed", "Extended", "SemiExtended")
1320
1321
1322class GSFontMaster(GSBase):
1323    _classesForName = {
1324        "alignmentZones": GSAlignmentZone,
1325        "ascender": float,
1326        "capHeight": float,
1327        "custom": unicode,
1328        "customValue": float,
1329        "customValue1": float,
1330        "customValue2": float,
1331        "customValue3": float,
1332        "customParameters": GSCustomParameter,
1333        "descender": float,
1334        "guideLines": GSGuideLine,
1335        "horizontalStems": int,
1336        "iconName": str,
1337        "id": str,
1338        "italicAngle": float,
1339        "name": unicode,
1340        "userData": dict,
1341        "verticalStems": int,
1342        "visible": bool,
1343        "weight": str,
1344        "weightValue": float,
1345        "width": str,
1346        "widthValue": float,
1347        "xHeight": float,
1348    }
1349    _defaultsForName = {
1350        # FIXME: (jany) In the latest Glyphs (1113), masters don't have a width
1351        # and weight anymore as attributes, even though those properties are
1352        # still written to the saved files.
1353        "weight": "Regular",
1354        "width": "Regular",
1355        "weightValue": 100.0,
1356        "widthValue": 100.0,
1357        "customValue": 0.0,
1358        "customValue1": 0.0,
1359        "customValue2": 0.0,
1360        "customValue3": 0.0,
1361        "xHeight": 500,
1362        "capHeight": 700,
1363        "ascender": 800,
1364        "descender": -200,
1365    }
1366    _wrapperKeysTranslate = {
1367        "guideLines": "guides",
1368        "custom": "customName",
1369        "name": "_name",
1370    }
1371    _keyOrder = (
1372        "alignmentZones",
1373        "ascender",
1374        "capHeight",
1375        "custom",
1376        "customValue",
1377        "customValue1",
1378        "customValue2",
1379        "customValue3",
1380        "customParameters",
1381        "descender",
1382        "guideLines",
1383        "horizontalStems",
1384        "iconName",
1385        "id",
1386        "italicAngle",
1387        "name",
1388        "userData",
1389        "verticalStems",
1390        "visible",
1391        "weight",
1392        "weightValue",
1393        "width",
1394        "widthValue",
1395        "xHeight",
1396    )
1397
1398    def __init__(self):
1399        super(GSFontMaster, self).__init__()
1400        self.id = str(uuid.uuid4())
1401        self.font = None
1402        self._name = None
1403        self._customParameters = []
1404        self.italicAngle = 0.0
1405        self._userData = None
1406        self.customName = ""
1407        for number in ("", "1", "2", "3"):
1408            setattr(self, "customValue" + number, 0.0)
1409
1410    def __repr__(self):
1411        return '<GSFontMaster "{}" width {} weight {}>'.format(
1412            self.name, self.widthValue, self.weightValue
1413        )
1414
1415    def shouldWriteValueForKey(self, key):
1416        if key in ("weight", "width"):
1417            return getattr(self, key) != "Regular"
1418        if key in ("xHeight", "capHeight", "ascender", "descender"):
1419            # Always write those values
1420            return True
1421        if key == "_name":
1422            # Only write out the name if we can't make it by joining the parts
1423            return self._name != self.name
1424        return super(GSFontMaster, self).shouldWriteValueForKey(key)
1425
1426    @property
1427    def name(self):
1428        name = self.customParameters["Master Name"]
1429        if name:
1430            return name
1431        if self._name:
1432            return self._name
1433        return self._joinName()
1434
1435    @name.setter
1436    def name(self, name):
1437        """This function will take the given name and split it into components
1438        weight, width, customName, and possibly the full name.
1439        This is what Glyphs 1113 seems to be doing, approximately.
1440        """
1441        weight, width, custom_name = self._splitName(name)
1442        self.set_all_name_components(name, weight, width, custom_name)
1443
1444    def set_all_name_components(self, name, weight, width, custom_name):
1445        """This function ensures that after being called, the master.name,
1446        master.weight, master.width, and master.customName match the given
1447        values.
1448        """
1449        self.weight = weight or "Regular"
1450        self.width = width or "Regular"
1451        self.customName = custom_name or ""
1452        # Only store the requested name if we can't build it from the parts
1453        if self._joinName() == name:
1454            self._name = None
1455            del self.customParameters["Master Name"]
1456        else:
1457            self._name = name
1458            self.customParameters["Master Name"] = name
1459
1460    def _joinName(self):
1461        # Remove None and empty string
1462        names = list(filter(None, [self.width, self.weight, self.customName]))
1463        # Remove redundant occurences of 'Regular'
1464        while len(names) > 1 and "Regular" in names:
1465            names.remove("Regular")
1466        if self.italicAngle:
1467            if names == ["Regular"]:
1468                return "Italic"
1469            names.append("Italic")
1470        return " ".join(names)
1471
1472    def _splitName(self, value):
1473        if value is None:
1474            value = ""
1475        weight = "Regular"
1476        width = "Regular"
1477        custom = ""
1478        names = []
1479        previous_was_removed = False
1480        for name in value.split(" "):
1481            if name == "Regular":
1482                pass
1483            elif name in MASTER_NAME_WEIGHTS:
1484                if previous_was_removed:
1485                    # Get the double space in custom
1486                    names.append("")
1487                previous_was_removed = True
1488                weight = name
1489            elif name in MASTER_NAME_WIDTHS:
1490                if previous_was_removed:
1491                    # Get the double space in custom
1492                    names.append("")
1493                previous_was_removed = True
1494                width = name
1495            else:
1496                previous_was_removed = False
1497                names.append(name)
1498        custom = " ".join(names).strip()
1499        return weight, width, custom
1500
1501    customParameters = property(
1502        lambda self: CustomParametersProxy(self),
1503        lambda self, value: CustomParametersProxy(self).setter(value),
1504    )
1505
1506    userData = property(
1507        lambda self: UserDataProxy(self),
1508        lambda self, value: UserDataProxy(self).setter(value),
1509    )
1510
1511
1512class GSNode(GSBase):
1513    _PLIST_VALUE_RE = re.compile(
1514        r'"([-.e\d]+) ([-.e\d]+) (LINE|CURVE|QCURVE|OFFCURVE|n/a)'
1515        r'(?: (SMOOTH))?(?: ({.*}))?"',
1516        re.DOTALL,
1517    )
1518    MOVE = "move"
1519    LINE = "line"
1520    CURVE = "curve"
1521    OFFCURVE = "offcurve"
1522    QCURVE = "qcurve"
1523    _parent = None
1524
1525    def __init__(self, position=(0, 0), nodetype=LINE, smooth=False, name=None):
1526        super(GSNode, self).__init__()
1527        self.position = Point(position[0], position[1])
1528        self.type = nodetype
1529        self.smooth = smooth
1530        self._parent = None
1531        self._userData = None
1532        self.name = name
1533
1534    def __repr__(self):
1535        content = self.type
1536        if self.smooth:
1537            content += " smooth"
1538        return "<{} {:g} {:g} {}>".format(
1539            self.__class__.__name__, self.position.x, self.position.y, content
1540        )
1541
1542    userData = property(
1543        lambda self: UserDataProxy(self),
1544        lambda self, value: UserDataProxy(self).setter(value),
1545    )
1546
1547    @property
1548    def parent(self):
1549        return self._parent
1550
1551    def plistValue(self):
1552        content = self.type.upper()
1553        if self.smooth:
1554            content += " SMOOTH"
1555        if self._userData is not None and len(self._userData) > 0:
1556            string = UnicodeIO()
1557            writer = Writer(string)
1558            writer.writeDict(self._userData)
1559            content += " "
1560            content += self._encode_dict_as_string(string.getvalue())
1561        return '"{} {} {}"'.format(
1562            floatToString(self.position[0]), floatToString(self.position[1]), content
1563        )
1564
1565    def read(self, line):
1566        m = self._PLIST_VALUE_RE.match(line).groups()
1567        self.position = Point(float(m[0]), float(m[1]))
1568        self.type = m[2].lower()
1569        self.smooth = bool(m[3])
1570
1571        if m[4] is not None and len(m[4]) > 0:
1572            value = self._decode_dict_as_string(m[4])
1573            parser = Parser()
1574            self._userData = parser.parse(value)
1575
1576        return self
1577
1578    @property
1579    def name(self):
1580        if "name" in self.userData:
1581            return self.userData["name"]
1582        return None
1583
1584    @name.setter
1585    def name(self, value):
1586        if value is None:
1587            if "name" in self.userData:
1588                del (self.userData["name"])
1589        else:
1590            self.userData["name"] = value
1591
1592    @property
1593    def index(self):
1594        assert self.parent
1595        return self.parent.nodes.index(self)
1596
1597    @property
1598    def nextNode(self):
1599        assert self.parent
1600        index = self.index
1601        if index == (len(self.parent.nodes) - 1):
1602            return self.parent.nodes[0]
1603        elif index < len(self.parent.nodes):
1604            return self.parent.nodes[index + 1]
1605
1606    @property
1607    def prevNode(self):
1608        assert self.parent
1609        index = self.index
1610        if index == 0:
1611            return self.parent.nodes[-1]
1612        elif index < len(self.parent.nodes):
1613            return self.parent.nodes[index - 1]
1614
1615    def makeNodeFirst(self):
1616        assert self.parent
1617        if self.type == "offcurve":
1618            raise ValueError("Off-curve points cannot become start points.")
1619        nodes = self.parent.nodes
1620        index = self.index
1621        newNodes = nodes[index : len(nodes)] + nodes[0:index]
1622        self.parent.nodes = newNodes
1623
1624    def toggleConnection(self):
1625        self.smooth = not self.smooth
1626
1627    # TODO
1628    @property
1629    def connection(self):
1630        raise NotImplementedError
1631
1632    # TODO
1633    @property
1634    def selected(self):
1635        raise OnlyInGlyphsAppError
1636
1637    @staticmethod
1638    def _encode_dict_as_string(value):
1639        """Takes the PLIST string of a dict, and returns the same string
1640        encoded such that it can be included in the string representation
1641        of a GSNode."""
1642        # Strip the first and last newlines
1643        if value.startswith("{\n"):
1644            value = "{" + value[2:]
1645        if value.endswith("\n}"):
1646            value = value[:-2] + "}"
1647        # escape double quotes and newlines
1648        return value.replace('"', '\\"').replace("\\n", "\\\\n").replace("\n", "\\n")
1649
1650    _ESCAPED_CHAR_RE = re.compile(r'\\(\\*)(?:(n)|("))')
1651
1652    @staticmethod
1653    def _unescape_char(m):
1654        backslashes = m.group(1) or ""
1655        if m.group(2):
1656            return "\n" if not backslashes else backslashes + "n"
1657        else:
1658            return backslashes + '"'
1659
1660    @classmethod
1661    def _decode_dict_as_string(cls, value):
1662        """Reverse function of _encode_string_as_dict"""
1663        # strip one level of backslashes preceding quotes and newlines
1664        return cls._ESCAPED_CHAR_RE.sub(cls._unescape_char, value)
1665
1666    def _indices(self):
1667        """Find the path_index and node_index that identify the given node."""
1668        path = self.parent
1669        layer = path.parent
1670        for path_index in range(len(layer.paths)):
1671            if path == layer.paths[path_index]:
1672                for node_index in range(len(path.nodes)):
1673                    if self == path.nodes[node_index]:
1674                        return Point(path_index, node_index)
1675        return None
1676
1677
1678class GSPath(GSBase):
1679    _classesForName = {"nodes": GSNode, "closed": bool}
1680    _defaultsForName = {"closed": True}
1681    _parent = None
1682
1683    def __init__(self):
1684        super(GSPath, self).__init__()
1685        self.nodes = []
1686
1687    @property
1688    def parent(self):
1689        return self._parent
1690
1691    def shouldWriteValueForKey(self, key):
1692        if key == "closed":
1693            return True
1694        return super(GSPath, self).shouldWriteValueForKey(key)
1695
1696    nodes = property(
1697        lambda self: PathNodesProxy(self),
1698        lambda self, value: PathNodesProxy(self).setter(value),
1699    )
1700
1701    @property
1702    def segments(self):
1703        self._segments = []
1704        self._segmentLength = 0
1705
1706        nodeCount = 0
1707        segmentCount = 0
1708        while nodeCount < len(self.nodes):
1709            newSegment = segment()
1710            newSegment.parent = self
1711            newSegment.index = segmentCount
1712
1713            if nodeCount == 0:
1714                newSegment.appendNode(self.nodes[-1])
1715            else:
1716                newSegment.appendNode(self.nodes[nodeCount - 1])
1717
1718            if self.nodes[nodeCount].type == "offcurve":
1719                newSegment.appendNode(self.nodes[nodeCount])
1720                newSegment.appendNode(self.nodes[nodeCount + 1])
1721                newSegment.appendNode(self.nodes[nodeCount + 2])
1722                nodeCount += 3
1723            elif self.nodes[nodeCount].type == "line":
1724                newSegment.appendNode(self.nodes[nodeCount])
1725                nodeCount += 1
1726
1727            self._segments.append(newSegment)
1728            self._segmentLength += 1
1729            segmentCount += 1
1730
1731        return self._segments
1732
1733    @segments.setter
1734    def segments(self, value):
1735        if type(value) in (list, tuple):
1736            self.setSegments(value)
1737        else:
1738            raise TypeError
1739
1740    def setSegments(self, segments):
1741        self.nodes = []
1742        for segment in segments:
1743            if len(segment.nodes) == 2 or len(segment.nodes) == 4:
1744                self.nodes.extend(segment.nodes[1:])
1745            else:
1746                raise ValueError
1747
1748    @property
1749    def bounds(self):
1750        left, bottom, right, top = None, None, None, None
1751        for segment in self.segments:
1752            newLeft, newBottom, newRight, newTop = segment.bbox()
1753            if left is None:
1754                left = newLeft
1755            else:
1756                left = min(left, newLeft)
1757            if bottom is None:
1758                bottom = newBottom
1759            else:
1760                bottom = min(bottom, newBottom)
1761            if right is None:
1762                right = newRight
1763            else:
1764                right = max(right, newRight)
1765            if top is None:
1766                top = newTop
1767            else:
1768                top = max(top, newTop)
1769        return Rect(Point(left, bottom), Point(right - left, top - bottom))
1770
1771    @property
1772    def direction(self):
1773        direction = 0
1774        for i in range(len(self.nodes)):
1775            thisNode = self.nodes[i]
1776            nextNode = thisNode.nextNode
1777            direction += (nextNode.position.x - thisNode.position.x) * (
1778                nextNode.position.y + thisNode.position.y
1779            )
1780        if direction < 0:
1781            return -1
1782        else:
1783            return 1
1784
1785    @property
1786    def selected(self):
1787        raise OnlyInGlyphsAppError
1788
1789    @property
1790    def bezierPath(self):
1791        raise OnlyInGlyphsAppError
1792
1793    def reverse(self):
1794        segments = list(reversed(self.segments))
1795        for s, segment in enumerate(segments):
1796            segment.nodes = list(reversed(segment.nodes))
1797            if s == len(segments) - 1:
1798                nextSegment = segments[0]
1799            else:
1800                nextSegment = segments[s + 1]
1801            if len(segment.nodes) == 2 and segment.nodes[-1].type == "curve":
1802                segment.nodes[-1].type = "line"
1803                nextSegment.nodes[0].type = "line"
1804            elif len(segment.nodes) == 4 and segment.nodes[-1].type == "line":
1805                segment.nodes[-1].type = "curve"
1806                nextSegment.nodes[0].type = "curve"
1807        self.setSegments(segments)
1808
1809    # TODO
1810    def addNodesAtExtremes(self):
1811        raise NotImplementedError
1812
1813    # TODO
1814    def applyTransform(self, transformationMatrix):
1815        raise NotImplementedError
1816
1817        # Using both skew values (>0.0) produces different results than Glyphs.
1818        # Skewing just on of the two works.
1819        # Needs more attention.
1820        assert len(transformationMatrix) == 6
1821        for node in self.nodes:
1822            transformation = (
1823                Affine.translation(transformationMatrix[4], transformationMatrix[5])
1824                * Affine.scale(transformationMatrix[0], transformationMatrix[3])
1825                * Affine.shear(
1826                    transformationMatrix[2] * 45.0, transformationMatrix[1] * 45.0
1827                )
1828            )
1829            x, y = (node.position.x, node.position.y) * transformation
1830            node.position.x = x
1831            node.position.y = y
1832
1833
1834class segment(list):
1835    def appendNode(self, node):
1836        if not hasattr(
1837            self, "nodes"
1838        ):  # instead of defining this in __init__(), because I hate super()
1839            self.nodes = []
1840        self.nodes.append(node)
1841        self.append(Point(node.position.x, node.position.y))
1842
1843    @property
1844    def nextSegment(self):
1845        assert self.parent
1846        index = self.index
1847        if index == (len(self.parent._segments) - 1):
1848            return self.parent._segments[0]
1849        elif index < len(self.parent._segments):
1850            return self.parent._segments[index + 1]
1851
1852    @property
1853    def prevSegment(self):
1854        assert self.parent
1855        index = self.index
1856        if index == 0:
1857            return self.parent._segments[-1]
1858        elif index < len(self.parent._segments):
1859            return self.parent._segments[index - 1]
1860
1861    def bbox(self):
1862        if len(self) == 2:
1863            left = min(self[0].x, self[1].x)
1864            bottom = min(self[0].y, self[1].y)
1865            right = max(self[0].x, self[1].x)
1866            top = max(self[0].y, self[1].y)
1867            return left, bottom, right, top
1868        elif len(self) == 4:
1869            left, bottom, right, top = self.bezierMinMax(
1870                self[0].x,
1871                self[0].y,
1872                self[1].x,
1873                self[1].y,
1874                self[2].x,
1875                self[2].y,
1876                self[3].x,
1877                self[3].y,
1878            )
1879            return left, bottom, right, top
1880        else:
1881            raise ValueError
1882
1883    def bezierMinMax(self, x0, y0, x1, y1, x2, y2, x3, y3):
1884        tvalues = []
1885        xvalues = []
1886        yvalues = []
1887
1888        for i in range(2):
1889            if i == 0:
1890                b = 6 * x0 - 12 * x1 + 6 * x2
1891                a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3
1892                c = 3 * x1 - 3 * x0
1893            else:
1894                b = 6 * y0 - 12 * y1 + 6 * y2
1895                a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3
1896                c = 3 * y1 - 3 * y0
1897
1898            if abs(a) < 1e-12:
1899                if abs(b) < 1e-12:
1900                    continue
1901                t = -c / b
1902                if 0 < t < 1:
1903                    tvalues.append(t)
1904                continue
1905
1906            b2ac = b * b - 4 * c * a
1907            if b2ac < 0:
1908                continue
1909            sqrtb2ac = math.sqrt(b2ac)
1910            t1 = (-b + sqrtb2ac) / (2 * a)
1911            if 0 < t1 < 1:
1912                tvalues.append(t1)
1913            t2 = (-b - sqrtb2ac) / (2 * a)
1914            if 0 < t2 < 1:
1915                tvalues.append(t2)
1916
1917        for j in range(len(tvalues) - 1, -1, -1):
1918            t = tvalues[j]
1919            mt = 1 - t
1920            newxValue = (
1921                (mt * mt * mt * x0)
1922                + (3 * mt * mt * t * x1)
1923                + (3 * mt * t * t * x2)
1924                + (t * t * t * x3)
1925            )
1926            if len(xvalues) > 0:
1927                xvalues[j] = newxValue
1928            else:
1929                xvalues.append(newxValue)
1930            newyValue = (
1931                (mt * mt * mt * y0)
1932                + (3 * mt * mt * t * y1)
1933                + (3 * mt * t * t * y2)
1934                + (t * t * t * y3)
1935            )
1936            if len(yvalues) > 0:
1937                yvalues[j] = newyValue
1938            else:
1939                yvalues.append(newyValue)
1940
1941        xvalues.append(x0)
1942        xvalues.append(x3)
1943        yvalues.append(y0)
1944        yvalues.append(y3)
1945
1946        return min(xvalues), min(yvalues), max(xvalues), max(yvalues)
1947
1948
1949class GSComponent(GSBase):
1950    _classesForName = {
1951        "alignment": int,
1952        "anchor": str,
1953        "locked": bool,
1954        "name": unicode,
1955        "piece": dict,
1956        "transform": Transform,
1957    }
1958    _wrapperKeysTranslate = {"piece": "smartComponentValues"}
1959    _defaultsForName = {"transform": Transform(1, 0, 0, 1, 0, 0)}
1960    _parent = None
1961
1962    # TODO: glyph arg is required
1963    def __init__(self, glyph="", offset=(0, 0), scale=(1, 1), transform=None):
1964        super(GSComponent, self).__init__()
1965
1966        if transform is None:
1967            if scale != (1, 1) or offset != (0, 0):
1968                xx, yy = scale
1969                dx, dy = offset
1970                self.transform = Transform(xx, 0, 0, yy, dx, dy)
1971        else:
1972            self.transform = transform
1973
1974        if isinstance(glyph, (str, unicode)):
1975            self.name = glyph
1976        elif isinstance(glyph, GSGlyph):
1977            self.name = glyph.name
1978
1979    def __repr__(self):
1980        return '<GSComponent "{}" x={:.1f} y={:.1f}>'.format(
1981            self.name, self.transform[4], self.transform[5]
1982        )
1983
1984    def shouldWriteValueForKey(self, key):
1985        if key == "piece":
1986            value = self.smartComponentValues
1987            return len(value) > 0
1988        return super(GSComponent, self).shouldWriteValueForKey(key)
1989
1990    @property
1991    def parent(self):
1992        return self._parent
1993
1994    # .position
1995    @property
1996    def position(self):
1997        return Point(self.transform[4], self.transform[5])
1998
1999    @position.setter
2000    def position(self, value):
2001        self.transform[4] = value[0]
2002        self.transform[5] = value[1]
2003
2004    # .scale
2005    @property
2006    def scale(self):
2007        self._sX, self._sY, self._R = transformStructToScaleAndRotation(
2008            self.transform.value
2009        )
2010        return self._sX, self._sY
2011
2012    @scale.setter
2013    def scale(self, value):
2014        self._sX, self._sY, self._R = transformStructToScaleAndRotation(
2015            self.transform.value
2016        )
2017        if type(value) in [int, float]:
2018            self._sX = value
2019            self._sY = value
2020        elif type(value) in [tuple, list] and len(value) == 2:
2021            self._sX, self._sY = value
2022        else:
2023            raise ValueError
2024        self.updateAffineTransform()
2025
2026    # .rotation
2027    @property
2028    def rotation(self):
2029        self._sX, self._sY, self._R = transformStructToScaleAndRotation(
2030            self.transform.value
2031        )
2032        return self._R
2033
2034    @rotation.setter
2035    def rotation(self, value):
2036        self._sX, self._sY, self._R = transformStructToScaleAndRotation(
2037            self.transform.value
2038        )
2039        self._R = value
2040        self.updateAffineTransform()
2041
2042    def updateAffineTransform(self):
2043        affine = list(
2044            Affine.translation(self.transform[4], self.transform[5])
2045            * Affine.scale(self._sX, self._sY)
2046            * Affine.rotation(self._R)
2047        )[:6]
2048        self.transform = Transform(
2049            affine[0], affine[1], affine[3], affine[4], affine[2], affine[5]
2050        )
2051
2052    @property
2053    def componentName(self):
2054        return self.name
2055
2056    @componentName.setter
2057    def componentName(self, value):
2058        self.name = value
2059
2060    @property
2061    def component(self):
2062        return self.parent.parent.parent.glyphs[self.name]
2063
2064    @property
2065    def layer(self):
2066        return self.parent.parent.parent.glyphs[self.name].layers[self.parent.layerId]
2067
2068    def applyTransformation(self, x, y):
2069        x *= self.scale[0]
2070        y *= self.scale[1]
2071        x += self.position.x
2072        y += self.position.y
2073        # TODO:
2074        # Integrate rotation
2075        return x, y
2076
2077    @property
2078    def bounds(self):
2079        bounds = self.layer.bounds
2080        if bounds is not None:
2081            left, bottom, width, height = self.layer.bounds
2082            right = left + width
2083            top = bottom + height
2084
2085            left, bottom = self.applyTransformation(left, bottom)
2086            right, top = self.applyTransformation(right, top)
2087
2088            if (
2089                left is not None
2090                and bottom is not None
2091                and right is not None
2092                and top is not None
2093            ):
2094                return Rect(Point(left, bottom), Point(right - left, top - bottom))
2095
2096    # smartComponentValues = property(
2097    #     lambda self: self.piece,
2098    #     lambda self, value: setattr(self, "piece", value))
2099
2100
2101class GSSmartComponentAxis(GSBase):
2102    _classesForName = {
2103        "name": unicode,
2104        "bottomName": unicode,
2105        "bottomValue": float,
2106        "topName": unicode,
2107        "topValue": float,
2108    }
2109    _keyOrder = ("name", "bottomName", "bottomValue", "topName", "topValue")
2110
2111    def shouldWriteValueForKey(self, key):
2112        if key in ("bottomValue", "topValue"):
2113            return True
2114        return super(GSSmartComponentAxis, self).shouldWriteValueForKey(key)
2115
2116
2117class GSAnchor(GSBase):
2118    _classesForName = {"name": unicode, "position": Point}
2119    _parent = None
2120    _defaultsForName = {"position": Point(0, 0)}
2121
2122    def __init__(self, name=None, position=None):
2123        super(GSAnchor, self).__init__()
2124        if name is not None:
2125            self.name = name
2126        if position is not None:
2127            self.position = position
2128
2129    def __repr__(self):
2130        return '<{} "{}" x={:.1f} y={:.1f}>'.format(
2131            self.__class__.__name__, self.name, self.position[0], self.position[1]
2132        )
2133
2134    def shouldWriteValueForKey(self, key):
2135        if key == "position":
2136            return True
2137        return super(GSAnchor, self).shouldWriteValueForKey(key)
2138
2139    @property
2140    def parent(self):
2141        return self._parent
2142
2143
2144class GSHint(GSBase):
2145    _classesForName = {
2146        "horizontal": bool,
2147        "options": int,  # bitfield
2148        "origin": Point,  # Index path to node
2149        "other1": Point,  # Index path to node for third node
2150        "other2": Point,  # Index path to node for fourth node
2151        "place": Point,  # (position, width)
2152        "scale": Point,  # for corners
2153        "stem": int,  # index of stem
2154        "target": parse_hint_target,  # Index path to node or 'up'/'down'
2155        "type": str,
2156        "name": unicode,
2157        "settings": dict,
2158    }
2159    _defaultsForName = {
2160        # TODO: (jany) check defaults in glyphs
2161        "origin": None,
2162        "other1": None,
2163        "other2": None,
2164        "place": None,
2165        "scale": None,
2166        "stem": -2,
2167    }
2168    _keyOrder = (
2169        "horizontal",
2170        "origin",
2171        "place",
2172        "target",
2173        "other1",
2174        "other2",
2175        "scale",
2176        "type",
2177        "stem",
2178        "name",
2179        "options",
2180        "settings",
2181    )
2182
2183    def shouldWriteValueForKey(self, key):
2184        if key == "settings" and (self.settings is None or len(self.settings) == 0):
2185            return None
2186        return super(GSHint, self).shouldWriteValueForKey(key)
2187
2188    def _origin_pos(self):
2189        if self.originNode:
2190            if self.horizontal:
2191                return self.originNode.position.y
2192            else:
2193                return self.originNode.position.x
2194        return self.origin
2195
2196    def _width_pos(self):
2197        if self.targetNode:
2198            if self.horizontal:
2199                return self.targetNode.position.y
2200            else:
2201                return self.targetNode.position.x
2202        return self.width
2203
2204    def __repr__(self):
2205        if self.horizontal:
2206            direction = "horizontal"
2207        else:
2208            direction = "vertical"
2209        if self.type == "BOTTOMGHOST" or self.type == "TOPGHOST":
2210            return "<GSHint {} origin=({})>".format(self.type, self._origin_pos())
2211        elif self.type == "STEM":
2212            return "<GSHint {} Stem origin=({}) target=({})>".format(
2213                direction, self._origin_pos(), self._width_pos()
2214            )
2215        elif self.type == "CORNER" or self.type == "CAP":
2216            return "<GSHint {} {}>".format(self.type, self.name)
2217        else:
2218            return "<GSHint {} {}>".format(self.type, direction)
2219
2220    @property
2221    def parent(self):
2222        return self._parent
2223
2224    @property
2225    def originNode(self):
2226        if self._originNode is not None:
2227            return self._originNode
2228        if self._origin is not None:
2229            return self.parent._find_node_by_indices(self._origin)
2230
2231    @originNode.setter
2232    def originNode(self, node):
2233        self._originNode = node
2234        self._origin = None
2235
2236    @property
2237    def origin(self):
2238        if self._origin is not None:
2239            return self._origin
2240        if self._originNode is not None:
2241            return self._originNode._indices()
2242
2243    @origin.setter
2244    def origin(self, origin):
2245        self._origin = origin
2246        self._originNode = None
2247
2248    @property
2249    def targetNode(self):
2250        if self._targetNode is not None:
2251            return self._targetNode
2252        if self._target is not None:
2253            return self.parent._find_node_by_indices(self._target)
2254
2255    @targetNode.setter
2256    def targetNode(self, node):
2257        self._targetNode = node
2258        self._target = None
2259
2260    @property
2261    def target(self):
2262        if self._target is not None:
2263            return self._target
2264        if self._targetNode is not None:
2265            return self._targetNode._indices()
2266
2267    @target.setter
2268    def target(self, target):
2269        self._target = target
2270        self._targetNode = None
2271
2272    @property
2273    def otherNode1(self):
2274        if self._otherNode1 is not None:
2275            return self._otherNode1
2276        if self._other1 is not None:
2277            return self.parent._find_node_by_indices(self._other1)
2278
2279    @otherNode1.setter
2280    def otherNode1(self, node):
2281        self._otherNode1 = node
2282        self._other1 = None
2283
2284    @property
2285    def other1(self):
2286        if self._other1 is not None:
2287            return self._other1
2288        if self._otherNode1 is not None:
2289            return self._otherNode1._indices()
2290
2291    @other1.setter
2292    def other1(self, other1):
2293        self._other1 = other1
2294        self._otherNode1 = None
2295
2296    @property
2297    def otherNode2(self):
2298        if self._otherNode2 is not None:
2299            return self._otherNode2
2300        if self._other2 is not None:
2301            return self.parent._find_node_by_indices(self._other2)
2302
2303    @otherNode2.setter
2304    def otherNode2(self, node):
2305        self._otherNode2 = node
2306        self._other2 = None
2307
2308    @property
2309    def other2(self):
2310        if self._other2 is not None:
2311            return self._other2
2312        if self._otherNode2 is not None:
2313            return self._otherNode2._indices()
2314
2315    @other2.setter
2316    def other2(self, other2):
2317        self._other2 = other2
2318        self._otherNode2 = None
2319
2320
2321class GSFeature(GSBase):
2322    _classesForName = {
2323        "automatic": bool,
2324        "code": unicode,
2325        "name": str,
2326        "notes": unicode,
2327        "disabled": bool,
2328    }
2329
2330    def __init__(self, name="xxxx", code=""):
2331        super(GSFeature, self).__init__()
2332        self.name = name
2333        self.code = code
2334
2335    def shouldWriteValueForKey(self, key):
2336        if key == "code":
2337            return True
2338        return super(GSFeature, self).shouldWriteValueForKey(key)
2339
2340    def getCode(self):
2341        return self._code
2342
2343    def setCode(self, code):
2344        replacements = (
2345            ("\\012", "\n"),
2346            ("\\011", "\t"),
2347            ("\\U2018", "'"),
2348            ("\\U2019", "'"),
2349            ("\\U201C", '"'),
2350            ("\\U201D", '"'),
2351        )
2352        for escaped, unescaped in replacements:
2353            code = code.replace(escaped, unescaped)
2354        self._code = code
2355
2356    code = property(getCode, setCode)
2357
2358    def __repr__(self):
2359        return '<{} "{}">'.format(self.__class__.__name__, self.name)
2360
2361    @property
2362    def parent(self):
2363        return self._parent
2364
2365
2366class GSClass(GSFeature):
2367    pass
2368
2369
2370class GSFeaturePrefix(GSFeature):
2371    pass
2372
2373
2374class GSAnnotation(GSBase):
2375    _classesForName = {
2376        "angle": float,
2377        "position": Point,
2378        "text": unicode,
2379        "type": str,
2380        "width": float,  # the width of the text field or size of the cicle
2381    }
2382    _defaultsForName = {
2383        "angle": 0.0,
2384        "position": Point(),
2385        "text": None,
2386        "type": 0,
2387        "width": 100.0,
2388    }
2389    _parent = None
2390
2391    @property
2392    def parent(self):
2393        return self._parent
2394
2395
2396class GSInstance(GSBase):
2397    _classesForName = {
2398        "customParameters": GSCustomParameter,
2399        "active": bool,
2400        "exports": bool,
2401        "instanceInterpolations": dict,
2402        "interpolationCustom": float,
2403        "interpolationCustom1": float,
2404        "interpolationCustom2": float,
2405        "interpolationCustom3": float,
2406        "interpolationWeight": float,
2407        "interpolationWidth": float,
2408        "isBold": bool,
2409        "isItalic": bool,
2410        "linkStyle": unicode,
2411        "manualInterpolation": bool,
2412        "name": unicode,
2413        "weightClass": unicode,
2414        "widthClass": unicode,
2415    }
2416    _defaultsForName = {
2417        "active": True,
2418        "exports": True,
2419        "interpolationCustom": 0.0,
2420        "interpolationCustom1": 0.0,
2421        "interpolationCustom2": 0.0,
2422        "interpolationCustom3": 0.0,
2423        "interpolationWeight": 100.0,
2424        "interpolationWidth": 100.0,
2425        "weightClass": "Regular",
2426        "widthClass": "Medium (normal)",
2427        "instanceInterpolations": {},
2428    }
2429    _keyOrder = (
2430        "active",
2431        "exports",
2432        "customParameters",
2433        "interpolationCustom",
2434        "interpolationCustom1",
2435        "interpolationCustom2",
2436        "interpolationCustom3",
2437        "interpolationWeight",
2438        "interpolationWidth",
2439        "instanceInterpolations",
2440        "isBold",
2441        "isItalic",
2442        "linkStyle",
2443        "manualInterpolation",
2444        "name",
2445        "weightClass",
2446        "widthClass",
2447    )
2448    _wrapperKeysTranslate = {
2449        "weightClass": "weight",
2450        "widthClass": "width",
2451        "interpolationWeight": "weightValue",
2452        "interpolationWidth": "widthValue",
2453        "interpolationCustom": "customValue",
2454        "interpolationCustom1": "customValue1",
2455        "interpolationCustom2": "customValue2",
2456        "interpolationCustom3": "customValue3",
2457    }
2458
2459    def __init__(self):
2460        super(GSInstance, self).__init__()
2461        # TODO: (jany) review this and move as much as possible into
2462        #       "_defaultsForKey"
2463        self.name = "Regular"
2464        self.custom = None
2465        self.linkStyle = ""
2466        self.visible = True
2467        self.isBold = False
2468        self.isItalic = False
2469        self._customParameters = []
2470
2471    customParameters = property(
2472        lambda self: CustomParametersProxy(self),
2473        lambda self, value: CustomParametersProxy(self).setter(value),
2474    )
2475
2476    @property
2477    def exports(self):
2478        """Deprecated alias for `active`, which is in the documentation."""
2479        return self.active
2480
2481    @exports.setter
2482    def exports(self, value):
2483        self.active = value
2484
2485    @property
2486    def familyName(self):
2487        value = self.customParameters["familyName"]
2488        if value:
2489            return value
2490        return self.parent.familyName
2491
2492    @familyName.setter
2493    def familyName(self, value):
2494        self.customParameters["familyName"] = value
2495
2496    @property
2497    def preferredFamily(self):
2498        value = self.customParameters["preferredFamily"]
2499        if value:
2500            return value
2501        return self.parent.familyName
2502
2503    @preferredFamily.setter
2504    def preferredFamily(self, value):
2505        self.customParameters["preferredFamily"] = value
2506
2507    @property
2508    def preferredSubfamilyName(self):
2509        value = self.customParameters["preferredSubfamilyName"]
2510        if value:
2511            return value
2512        return self.name
2513
2514    @preferredSubfamilyName.setter
2515    def preferredSubfamilyName(self, value):
2516        self.customParameters["preferredSubfamilyName"] = value
2517
2518    @property
2519    def windowsFamily(self):
2520        value = self.customParameters["styleMapFamilyName"]
2521        if value:
2522            return value
2523        if self.name not in ("Regular", "Bold", "Italic", "Bold Italic"):
2524            return self.familyName + " " + self.name
2525        else:
2526            return self.familyName
2527
2528    @windowsFamily.setter
2529    def windowsFamily(self, value):
2530        self.customParameters["styleMapFamilyName"] = value
2531
2532    @property
2533    def windowsStyle(self):
2534        if self.name in ("Regular", "Bold", "Italic", "Bold Italic"):
2535            return self.name
2536        else:
2537            return "Regular"
2538
2539    @property
2540    def windowsLinkedToStyle(self):
2541        value = self.linkStyle
2542        return value
2543        if self.name in ("Regular", "Bold", "Italic", "Bold Italic"):
2544            return self.name
2545        else:
2546            return "Regular"
2547
2548    @property
2549    def fontName(self):
2550        value = self.customParameters["postscriptFontName"]
2551        if value:
2552            return value
2553        # TODO: strip invalid characters
2554        return "".join(self.familyName.split(" ")) + "-" + self.name
2555
2556    @fontName.setter
2557    def fontName(self, value):
2558        self.customParameters["postscriptFontName"] = value
2559
2560    @property
2561    def fullName(self):
2562        value = self.customParameters["postscriptFullName"]
2563        if value:
2564            return value
2565        return self.familyName + " " + self.name
2566
2567    @fullName.setter
2568    def fullName(self, value):
2569        self.customParameters["postscriptFullName"] = value
2570
2571
2572class GSBackgroundImage(GSBase):
2573    _classesForName = {
2574        "crop": Rect,
2575        "imagePath": unicode,
2576        "locked": bool,
2577        "transform": Transform,
2578        "alpha": int,
2579    }
2580    _defaultsForName = {"alpha": 50, "transform": Transform(1, 0, 0, 1, 0, 0)}
2581    _wrapperKeysTranslate = {"alpha": "_alpha"}
2582
2583    def __init__(self, path=None):
2584        super(GSBackgroundImage, self).__init__()
2585        self.imagePath = path
2586        self._sX, self._sY, self._R = transformStructToScaleAndRotation(
2587            self.transform.value
2588        )
2589
2590    def __repr__(self):
2591        return "<GSBackgroundImage '%s'>" % self.imagePath
2592
2593    # .path
2594    @property
2595    def path(self):
2596        return self.imagePath
2597
2598    @path.setter
2599    def path(self, value):
2600        # FIXME: (jany) use posix pathnames here?
2601        # FIXME: (jany) the following code must have never been tested.
2602        #   Also it would require to keep track of the parent for background
2603        #   images.
2604        # if os.path.dirname(os.path.abspath(value)) == \
2605        #       os.path.dirname(os.path.abspath(self.parent.parent.parent.filepath)):
2606        #     self.imagePath = os.path.basename(value)
2607        # else:
2608        self.imagePath = value
2609
2610    # .position
2611    @property
2612    def position(self):
2613        return Point(self.transform[4], self.transform[5])
2614
2615    @position.setter
2616    def position(self, value):
2617        self.transform[4] = value[0]
2618        self.transform[5] = value[1]
2619
2620    # .scale
2621    @property
2622    def scale(self):
2623        return self._sX, self._sY
2624
2625    @scale.setter
2626    def scale(self, value):
2627        if type(value) in [int, float]:
2628            self._sX = value
2629            self._sY = value
2630        elif type(value) in [tuple, list] and len(value) == 2:
2631            self._sX, self._sY = value
2632        else:
2633            raise ValueError
2634        self.updateAffineTransform()
2635
2636    # .rotation
2637    @property
2638    def rotation(self):
2639        return self._R
2640
2641    @rotation.setter
2642    def rotation(self, value):
2643        self._R = value
2644        self.updateAffineTransform()
2645
2646    # .alpha
2647    @property
2648    def alpha(self):
2649        return self._alpha
2650
2651    @alpha.setter
2652    def alpha(self, value):
2653        if not 10 <= value <= 100:
2654            value = 50
2655        self._alpha = value
2656
2657    def updateAffineTransform(self):
2658        affine = list(
2659            Affine.translation(self.transform[4], self.transform[5])
2660            * Affine.scale(self._sX, self._sY)
2661            * Affine.rotation(self._R)
2662        )[:6]
2663        self.transform = Transform(
2664            affine[0], affine[1], affine[3], affine[4], affine[2], affine[5]
2665        )
2666
2667
2668class GSLayer(GSBase):
2669    _classesForName = {
2670        "anchors": GSAnchor,
2671        "annotations": GSAnnotation,
2672        "associatedMasterId": str,
2673        # The next line is added after we define GSBackgroundLayer
2674        # "background": GSBackgroundLayer,
2675        "backgroundImage": GSBackgroundImage,
2676        "color": parse_color,
2677        "components": GSComponent,
2678        "guideLines": GSGuideLine,
2679        "hints": GSHint,
2680        "layerId": str,
2681        "leftMetricsKey": unicode,
2682        "name": unicode,
2683        "paths": GSPath,
2684        "rightMetricsKey": unicode,
2685        "userData": dict,
2686        "vertWidth": float,
2687        "vertOrigin": float,
2688        "visible": bool,
2689        "width": float,
2690        "widthMetricsKey": unicode,
2691    }
2692    _defaultsForName = {
2693        "width": 600.0,
2694        "leftMetricsKey": None,
2695        "rightMetricsKey": None,
2696        "widthMetricsKey": None,
2697    }
2698    _wrapperKeysTranslate = {"guideLines": "guides", "background": "_background"}
2699    _keyOrder = (
2700        "anchors",
2701        "annotations",
2702        "associatedMasterId",
2703        "background",
2704        "backgroundImage",
2705        "color",
2706        "components",
2707        "guideLines",
2708        "hints",
2709        "layerId",
2710        "leftMetricsKey",
2711        "widthMetricsKey",
2712        "rightMetricsKey",
2713        "name",
2714        "paths",
2715        "userData",
2716        "visible",
2717        "vertOrigin",
2718        "vertWidth",
2719        "width",
2720    )
2721
2722    def __init__(self):
2723        super(GSLayer, self).__init__()
2724        self.parent = None
2725        self._anchors = []
2726        self._hints = []
2727        self._annotations = []
2728        self._components = []
2729        self._guides = []
2730        self._paths = []
2731        self._selection = []
2732        self._userData = None
2733        self._background = None
2734        self.backgroundImage = None
2735
2736    def __repr__(self):
2737        name = self.name
2738        try:
2739            # assert self.name
2740            name = self.name
2741        except AttributeError:
2742            name = "orphan (n)"
2743        try:
2744            assert self.parent.name
2745            parent = self.parent.name
2746        except (AttributeError, AssertionError):
2747            parent = "orphan"
2748        return '<{} "{}" ({})>'.format(self.__class__.__name__, name, parent)
2749
2750    def __lt__(self, other):
2751        if self.master and other.master and self.associatedMasterId == self.layerId:
2752            return (
2753                self.master.weightValue < other.master.weightValue
2754                or self.master.widthValue < other.master.widthValue
2755            )
2756
2757    @property
2758    def layerId(self):
2759        return self._layerId
2760
2761    @layerId.setter
2762    def layerId(self, value):
2763        self._layerId = value
2764        # Update the layer map in the parent glyph, if any.
2765        # The "hasattr" is here because this setter is called by the GSBase
2766        # __init__() method before the parent property is set.
2767        if hasattr(self, "parent") and self.parent:
2768            parent_layers = OrderedDict()
2769            updated = False
2770            for id, layer in self.parent._layers.items():
2771                if layer == self:
2772                    parent_layers[self._layerId] = self
2773                    updated = True
2774                else:
2775                    parent_layers[id] = layer
2776            if not updated:
2777                parent_layers[self._layerId] = self
2778            self.parent._layers = parent_layers
2779
2780    @property
2781    def master(self):
2782        if self.associatedMasterId and self.parent:
2783            master = self.parent.parent.masterForId(self.associatedMasterId)
2784            return master
2785
2786    def shouldWriteValueForKey(self, key):
2787        if key == "width":
2788            return True
2789        if key == "associatedMasterId":
2790            return self.layerId != self.associatedMasterId
2791        if key == "name":
2792            return (
2793                self.name is not None
2794                and len(self.name) > 0
2795                and self.layerId != self.associatedMasterId
2796            )
2797        return super(GSLayer, self).shouldWriteValueForKey(key)
2798
2799    @property
2800    def name(self):
2801        if (
2802            self.associatedMasterId
2803            and self.associatedMasterId == self.layerId
2804            and self.parent
2805        ):
2806            master = self.parent.parent.masterForId(self.associatedMasterId)
2807            if master:
2808                return master.name
2809        return self._name
2810
2811    @name.setter
2812    def name(self, value):
2813        self._name = value
2814
2815    anchors = property(
2816        lambda self: LayerAnchorsProxy(self),
2817        lambda self, value: LayerAnchorsProxy(self).setter(value),
2818    )
2819
2820    hints = property(
2821        lambda self: LayerHintsProxy(self),
2822        lambda self, value: LayerHintsProxy(self).setter(value),
2823    )
2824
2825    paths = property(
2826        lambda self: LayerPathsProxy(self),
2827        lambda self, value: LayerPathsProxy(self).setter(value),
2828    )
2829
2830    components = property(
2831        lambda self: LayerComponentsProxy(self),
2832        lambda self, value: LayerComponentsProxy(self).setter(value),
2833    )
2834
2835    guides = property(
2836        lambda self: LayerGuideLinesProxy(self),
2837        lambda self, value: LayerGuideLinesProxy(self).setter(value),
2838    )
2839
2840    annotations = property(
2841        lambda self: LayerAnnotationProxy(self),
2842        lambda self, value: LayerAnnotationProxy(self).setter(value),
2843    )
2844
2845    userData = property(
2846        lambda self: UserDataProxy(self),
2847        lambda self, value: UserDataProxy(self).setter(value),
2848    )
2849
2850    @property
2851    def smartComponentPoleMapping(self):
2852        if "PartSelection" not in self.userData:
2853            self.userData["PartSelection"] = {}
2854        return self.userData["PartSelection"]
2855
2856    @smartComponentPoleMapping.setter
2857    def smartComponentPoleMapping(self, value):
2858        self.userData["PartSelection"] = value
2859
2860    @property
2861    def bounds(self):
2862        left, bottom, right, top = None, None, None, None
2863
2864        for item in self.paths.values() + self.components.values():
2865
2866            newLeft, newBottom, newWidth, newHeight = item.bounds
2867            newRight = newLeft + newWidth
2868            newTop = newBottom + newHeight
2869
2870            if left is None:
2871                left = newLeft
2872            else:
2873                left = min(left, newLeft)
2874            if bottom is None:
2875                bottom = newBottom
2876            else:
2877                bottom = min(bottom, newBottom)
2878            if right is None:
2879                right = newRight
2880            else:
2881                right = max(right, newRight)
2882            if top is None:
2883                top = newTop
2884            else:
2885                top = max(top, newTop)
2886
2887        if (
2888            left is not None
2889            and bottom is not None
2890            and right is not None
2891            and top is not None
2892        ):
2893            return Rect(Point(left, bottom), Point(right - left, top - bottom))
2894
2895    def _find_node_by_indices(self, point):
2896        """"Find the GSNode that is refered to by the given indices.
2897
2898        See GSNode::_indices()
2899        """
2900        path_index, node_index = point
2901        path = self.paths[int(path_index)]
2902        node = path.nodes[int(node_index)]
2903        return node
2904
2905    @property
2906    def background(self):
2907        """Only a getter on purpose. See the tests."""
2908        if self._background is None:
2909            self._background = GSBackgroundLayer()
2910            self._background._foreground = self
2911        return self._background
2912
2913    # FIXME: (jany) how to check whether there is a background without calling
2914    #               ::background?
2915    @property
2916    def hasBackground(self):
2917        return bool(self._background)
2918
2919    @property
2920    def foreground(self):
2921        """Forbidden, and also forbidden to set it."""
2922        raise AttributeError
2923
2924
2925class GSBackgroundLayer(GSLayer):
2926    def shouldWriteValueForKey(self, key):
2927        if key == "width":
2928            return False
2929        return super(GSBackgroundLayer, self).shouldWriteValueForKey(key)
2930
2931    @property
2932    def background(self):
2933        return None
2934
2935    @property
2936    def foreground(self):
2937        return self._foreground
2938
2939    # The width property of this class behaves like this in Glyphs:
2940    #  - Always returns 600.0
2941    #  - Settable but does not remember the value (basically useless)
2942    # Reproduce this behaviour here so that the roundtrip does not rely on it.
2943    @property
2944    def width(self):
2945        return 600.0
2946
2947    @width.setter
2948    def width(self, whatever):
2949        pass
2950
2951
2952GSLayer._classesForName["background"] = GSBackgroundLayer
2953
2954
2955class GSGlyph(GSBase):
2956    _classesForName = {
2957        "bottomKerningGroup": str,
2958        "bottomMetricsKey": str,
2959        "category": str,
2960        "color": parse_color,
2961        "export": bool,
2962        "glyphname": unicode,
2963        "lastChange": parse_datetime,
2964        "layers": GSLayer,
2965        "leftKerningGroup": unicode,
2966        "leftKerningKey": unicode,
2967        "leftMetricsKey": unicode,
2968        "note": unicode,
2969        "partsSettings": GSSmartComponentAxis,
2970        "production": str,
2971        "rightKerningGroup": unicode,
2972        "rightKerningKey": unicode,
2973        "rightMetricsKey": unicode,
2974        "script": str,
2975        "subCategory": str,
2976        "topKerningGroup": str,
2977        "topMetricsKey": str,
2978        "unicode": UnicodesList,
2979        "userData": dict,
2980        "vertWidthMetricsKey": str,
2981        "widthMetricsKey": unicode,
2982    }
2983    _wrapperKeysTranslate = {
2984        "unicode": "unicodes",
2985        "glyphname": "name",
2986        "partsSettings": "smartComponentAxes",
2987    }
2988    _defaultsForName = {
2989        "category": None,
2990        "color": None,
2991        "export": True,
2992        "lastChange": None,
2993        "leftKerningGroup": None,
2994        "leftMetricsKey": None,
2995        "name": None,
2996        "note": None,
2997        "rightKerningGroup": None,
2998        "rightMetricsKey": None,
2999        "script": None,
3000        "subCategory": None,
3001        "userData": None,
3002        "widthMetricsKey": None,
3003    }
3004    _keyOrder = (
3005        "color",
3006        "export",
3007        "glyphname",
3008        "production",
3009        "lastChange",
3010        "layers",
3011        "leftKerningGroup",
3012        "leftMetricsKey",
3013        "widthMetricsKey",
3014        "vertWidthMetricsKey",
3015        "note",
3016        "rightKerningGroup",
3017        "rightMetricsKey",
3018        "topKerningGroup",
3019        "topMetricsKey",
3020        "bottomKerningGroup",
3021        "bottomMetricsKey",
3022        "unicode",
3023        "script",
3024        "category",
3025        "subCategory",
3026        "userData",
3027        "partsSettings",
3028    )
3029
3030    def __init__(self, name=None):
3031        super(GSGlyph, self).__init__()
3032        self._layers = OrderedDict()
3033        self.name = name
3034        self.parent = None
3035        self.export = True
3036        self.selected = False
3037        self.smartComponentAxes = []
3038        self._userData = None
3039
3040    def __repr__(self):
3041        return '<GSGlyph "{}" with {} layers>'.format(self.name, len(self.layers))
3042
3043    def shouldWriteValueForKey(self, key):
3044        if key in ("script", "category", "subCategory"):
3045            return getattr(self, key) is not None
3046        return super(GSGlyph, self).shouldWriteValueForKey(key)
3047
3048    layers = property(
3049        lambda self: GlyphLayerProxy(self),
3050        lambda self, value: GlyphLayerProxy(self).setter(value),
3051    )
3052
3053    def _setupLayer(self, layer, key):
3054        assert isinstance(key, (str, unicode))
3055        layer.parent = self
3056        layer.layerId = key
3057        # TODO use proxy `self.parent.masters[key]`
3058        if self.parent and self.parent.masterForId(key):
3059            layer.associatedMasterId = key
3060
3061    # def setLayerForKey(self, layer, key):
3062    #     if Layer and Key:
3063    #         Layer.parent = self
3064    #         Layer.layerId = Key
3065    #         if self.parent.fontMasterForId(Key):
3066    #             Layer.associatedMasterId = Key
3067    #         self._layers[key] = layer
3068
3069    def removeLayerForKey_(self, key):
3070        for layer in list(self._layers):
3071            if layer == key:
3072                del self._layers[key]
3073
3074    @property
3075    def string(self):
3076        if self.unicode:
3077            return unichr(int(self.unicode, 16))
3078
3079    userData = property(
3080        lambda self: UserDataProxy(self),
3081        lambda self, value: UserDataProxy(self).setter(value),
3082    )
3083
3084    glyphname = property(
3085        lambda self: self.name, lambda self, value: setattr(self, "name", value)
3086    )
3087
3088    smartComponentAxes = property(
3089        lambda self: self.partsSettings,
3090        lambda self, value: setattr(self, "partsSettings", value),
3091    )
3092
3093    @property
3094    def id(self):
3095        """An unique identifier for each glyph"""
3096        return self.name
3097
3098    @property
3099    def unicode(self):
3100        if self._unicodes:
3101            return self._unicodes[0]
3102        return None
3103
3104    @unicode.setter
3105    def unicode(self, unicode):
3106        self._unicodes = UnicodesList(unicode)
3107
3108    @property
3109    def unicodes(self):
3110        return self._unicodes
3111
3112    @unicodes.setter
3113    def unicodes(self, unicodes):
3114        self._unicodes = UnicodesList(unicodes)
3115
3116
3117class GSFont(GSBase):
3118    _classesForName = {
3119        ".appVersion": str,
3120        "DisplayStrings": unicode,
3121        "classes": GSClass,
3122        "copyright": unicode,
3123        "customParameters": GSCustomParameter,
3124        "date": parse_datetime,
3125        "designer": unicode,
3126        "designerURL": unicode,
3127        "disablesAutomaticAlignment": bool,
3128        "disablesNiceNames": bool,
3129        "familyName": unicode,
3130        "featurePrefixes": GSFeaturePrefix,
3131        "features": GSFeature,
3132        "fontMaster": GSFontMaster,
3133        "glyphs": GSGlyph,
3134        "gridLength": int,
3135        "gridSubDivision": int,
3136        "instances": GSInstance,
3137        "keepAlternatesTogether": bool,
3138        "kerning": OrderedDict,
3139        "keyboardIncrement": float,
3140        "manufacturer": unicode,
3141        "manufacturerURL": unicode,
3142        "unitsPerEm": int,
3143        "userData": dict,
3144        "versionMajor": int,
3145        "versionMinor": int,
3146    }
3147    _wrapperKeysTranslate = {
3148        ".appVersion": "appVersion",
3149        "fontMaster": "masters",
3150        "unitsPerEm": "upm",
3151        "gridLength": "grid",
3152        "gridSubDivision": "gridSubDivisions",
3153    }
3154    _defaultsForName = {
3155        "classes": [],
3156        "customParameters": [],
3157        "disablesAutomaticAlignment": False,
3158        "disablesNiceNames": False,
3159        "gridLength": 1,
3160        "gridSubDivision": 1,
3161        "unitsPerEm": 1000,
3162        "kerning": OrderedDict(),
3163        "keyboardIncrement": 1,
3164    }
3165
3166    def __init__(self, path=None):
3167        super(GSFont, self).__init__()
3168
3169        self.familyName = "Unnamed font"
3170        self._versionMinor = 0
3171        self.versionMajor = 1
3172        self.appVersion = "895"  # minimum required version
3173        self._glyphs = []
3174        self._masters = []
3175        self._instances = []
3176        self._customParameters = []
3177        self._classes = []
3178        self.filepath = None
3179        self._userData = None
3180
3181        if path:
3182            # Support os.PathLike objects.
3183            # https://www.python.org/dev/peps/pep-0519/#backwards-compatibility
3184            if hasattr(path, "__fspath__"):
3185                path = path.__fspath__()
3186
3187            assert isinstance(path, (str, unicode)), "Please supply a file path"
3188            assert path.endswith(
3189                ".glyphs"
3190            ), "Please supply a file path to a .glyphs file"
3191            with open(path, "r", encoding="utf-8") as fp:
3192                p = Parser()
3193                logger.info('Parsing "%s" file into <GSFont>' % path)
3194                p.parse_into_object(self, fp.read())
3195            self.filepath = path
3196            for master in self.masters:
3197                master.font = self
3198
3199    def __repr__(self):
3200        return '<{} "{}">'.format(self.__class__.__name__, self.familyName)
3201
3202    def shouldWriteValueForKey(self, key):
3203        if key in ("unitsPerEm", "versionMajor", "versionMinor"):
3204            return True
3205        return super(GSFont, self).shouldWriteValueForKey(key)
3206
3207    def save(self, path=None):
3208        if path is None:
3209            if self.filepath:
3210                path = self.filepath
3211            else:
3212                raise ValueError("No path provided and GSFont has no filepath")
3213        with open(path, "w", encoding="utf-8") as fp:
3214            w = Writer(fp)
3215            logger.info("Writing %r to .glyphs file", self)
3216            w.write(self)
3217
3218    def getVersionMinor(self):
3219        return self._versionMinor
3220
3221    def setVersionMinor(self, value):
3222        """Ensure that the minor version number is between 0 and 999."""
3223        assert 0 <= value <= 999
3224        self._versionMinor = value
3225
3226    versionMinor = property(getVersionMinor, setVersionMinor)
3227
3228    glyphs = property(
3229        lambda self: FontGlyphsProxy(self),
3230        lambda self, value: FontGlyphsProxy(self).setter(value),
3231    )
3232
3233    def _setupGlyph(self, glyph):
3234        glyph.parent = self
3235        for layer in glyph.layers:
3236            if (
3237                not hasattr(layer, "associatedMasterId")
3238                or layer.associatedMasterId is None
3239                or len(layer.associatedMasterId) == 0
3240            ):
3241                glyph._setupLayer(layer, layer.layerId)
3242
3243    @property
3244    def features(self):
3245        return self._features
3246
3247    @features.setter
3248    def features(self, value):
3249        # FIXME: (jany) why not use Proxy like every other attribute?
3250        # FIXME: (jany) do the same for featurePrefixes?
3251        self._features = value
3252        for g in self._features:
3253            g._parent = self
3254
3255    masters = property(
3256        lambda self: FontFontMasterProxy(self),
3257        lambda self, value: FontFontMasterProxy(self).setter(value),
3258    )
3259
3260    def masterForId(self, key):
3261        for master in self._masters:
3262            if master.id == key:
3263                return master
3264        return None
3265
3266    # FIXME: (jany) Why is this not a FontInstanceProxy?
3267    @property
3268    def instances(self):
3269        return self._instances
3270
3271    @instances.setter
3272    def instances(self, value):
3273        self._instances = value
3274        for i in self._instances:
3275            i.parent = self
3276
3277    classes = property(
3278        lambda self: FontClassesProxy(self),
3279        lambda self, value: FontClassesProxy(self).setter(value),
3280    )
3281
3282    customParameters = property(
3283        lambda self: CustomParametersProxy(self),
3284        lambda self, value: CustomParametersProxy(self).setter(value),
3285    )
3286
3287    userData = property(
3288        lambda self: UserDataProxy(self),
3289        lambda self, value: UserDataProxy(self).setter(value),
3290    )
3291
3292    @property
3293    def kerning(self):
3294        return self._kerning
3295
3296    @kerning.setter
3297    def kerning(self, kerning):
3298        self._kerning = kerning
3299        for master_map in kerning.values():
3300            for glyph_map in master_map.values():
3301                for right_glyph, value in glyph_map.items():
3302                    glyph_map[right_glyph] = float(value)
3303
3304    @property
3305    def selection(self):
3306        return (glyph for glyph in self.glyphs if glyph.selected)
3307
3308    @property
3309    def note(self):
3310        value = self.customParameters["note"]
3311        if value:
3312            return value
3313        else:
3314            return ""
3315
3316    @note.setter
3317    def note(self, value):
3318        self.customParameters["note"] = value
3319
3320    @property
3321    def gridLength(self):
3322        if self.gridSubDivisions > 0:
3323            return self.grid / self.gridSubDivisions
3324        else:
3325            return self.grid
3326
3327    EMPTY_KERNING_VALUE = (1 << 63) - 1  # As per the documentation
3328
3329    def kerningForPair(self, fontMasterId, leftKey, rightKey, direction=LTR):
3330        # TODO: (jany) understand and use the direction parameter
3331        if not self._kerning:
3332            return self.EMPTY_KERNING_VALUE
3333        try:
3334            return self._kerning[fontMasterId][leftKey][rightKey]
3335        except KeyError:
3336            return self.EMPTY_KERNING_VALUE
3337
3338    def setKerningForPair(self, fontMasterId, leftKey, rightKey, value, direction=LTR):
3339        # TODO: (jany) understand and use the direction parameter
3340        if not self._kerning:
3341            self._kerning = {}
3342        if fontMasterId not in self._kerning:
3343            self._kerning[fontMasterId] = {}
3344        if leftKey not in self._kerning[fontMasterId]:
3345            self._kerning[fontMasterId][leftKey] = {}
3346        self._kerning[fontMasterId][leftKey][rightKey] = value
3347
3348    def removeKerningForPair(self, fontMasterId, leftKey, rightKey, direction=LTR):
3349        # TODO: (jany) understand and use the direction parameter
3350        if not self._kerning:
3351            return
3352        if fontMasterId not in self._kerning:
3353            return
3354        if leftKey not in self._kerning[fontMasterId]:
3355            return
3356        if rightKey not in self._kerning[fontMasterId][leftKey]:
3357            return
3358        del (self._kerning[fontMasterId][leftKey][rightKey])
3359        if not self._kerning[fontMasterId][leftKey]:
3360            del (self._kerning[fontMasterId][leftKey])
3361        if not self._kerning[fontMasterId]:
3362            del (self._kerning[fontMasterId])
3363