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