pax_global_header00006660000000000000000000000064132234161620014512gustar00rootroot0000000000000052 comment=183e727fc65c92c7d5c5156d39296957b2e84386 glyphslib-2.2.1/000077500000000000000000000000001322341616200135115ustar00rootroot00000000000000glyphslib-2.2.1/.codecov.yml000066400000000000000000000001151322341616200157310ustar00rootroot00000000000000comment: false coverage: status: project: off patch: off glyphslib-2.2.1/.coveragerc000066400000000000000000000015041322341616200156320ustar00rootroot00000000000000[run] # measure 'branch' coverage in addition to 'statement' coverage # See: http://coverage.readthedocs.org/en/coverage-4.0.3/branch.html#branch branch = True # list of directories or packages to measure source = glyphsLib # these are treated as equivalent when combining data [paths] source = Lib/glyphsLib .tox/*/lib/python*/site-packages/glyphsLib .tox/pypy*/site-packages/glyphsLib [report] # Regexes for lines to exclude from consideration exclude_lines = # keywords to use in inline comments to skip coverage pragma: no cover # don't complain if tests don't hit defensive assertion code raise AssertionError raise NotImplementedError # don't complain if non-runnable code isn't run if 0: if __name__ == .__main__.: # ignore source code that can’t be found ignore_errors = True glyphslib-2.2.1/.gitignore000066400000000000000000000003721322341616200155030ustar00rootroot00000000000000# Byte-compiled files __pycache__/ *.pyc # Packaging *.egg *.egg-info *.eggs build dist # Unit test .cache/ .tox/ .coverage htmlcov # Autosaved files *~ # Additional test files # (there's a script to download them from GitHub) tests/noto-source* glyphslib-2.2.1/.pyup.yml000066400000000000000000000002501322341616200153040ustar00rootroot00000000000000# controls the frequency of updates (undocumented beta feature) schedule: every week # do not pin dependencies unless they have explicit version specifiers pin: False glyphslib-2.2.1/.travis.yml000066400000000000000000000021601322341616200156210ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.6" branches: only: - master - /^v\d+\.\d+.*$/ install: pip install tox-travis script: tox after_success: tox -e codecov deploy: # deploy to PyPI on tags provider: pypi server: https://upload.pypi.org/legacy/ on: repo: googlei18n/glyphsLib tags: true all_branches: true python: 3.6 user: anthrotype password: secure: MFfm+XsmNZmrFzmIytS5MhoIcSHle695sv036Q5pKugtBBn187fSBR/zCfM0C2qRy7rFUyCXKJAhiVb94VCsr3B+lFM03A5uSWLzDXPk3qGGkut39+BQU3wQ3YZx5ozo/PHOWPTo9CdHHmFFgeWJDo+pj6t9rE8m2MYFEf6QC9VGE2Tn4pgMOqRsHqtiRoV0sTe0Ulkek1c9iWR6Lpw5Eq+BgdFFaqYx1vjMywGwrlzgu5Q37G7QZ6C4X1UbzERXcoexDC6Qlrt/DGKsh4NEENfsE43b7llVMpWwxs/FxKt4fJA1cMacDQdwLc7iIyplTzdKYfau2qg07hKAfC+q/jG7D/g8K/K9nROVGwJCaWM2cdnlvMFRfoznYx3l0Qb8Kqf0uFahdSY3k3f60Fi4kF2v5ZWUIRlBm6Lb15h+mfIefqUbKy4V9ptzsUMXYGC3nutWdqDPNF8KUoC/LCykgDQ17ZK7knPUl40Ivj2PuVy6fyUvxEGh2+2pE1Ku2Lk1m7eE/AW2s7Zx0gWQ+xR++kTgc6XQZuGJDWKeYwKCPFopENwPkHX76dJpV6U8ALirYYyro4noB8V3RmNsJbqfcCB/aVbrmT97eb5M7R0W3H5CNhnXWtjxqYp16KHzBqoPYNpuYQLpky5lNjpE0KI4RXF/eb3ywLL/MVs0Z1Vklfw= distributions: sdist bdist_wheel glyphslib-2.2.1/CONTRIBUTING.md000066400000000000000000000026521322341616200157470ustar00rootroot00000000000000Want to contribute? Great! First, read this page (including the small print at the end). ### Before you contribute Before we can use your code, you must sign the [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) (CLA), which you can do online. The CLA is necessary mainly because you own the copyright to your changes, even after your contribution becomes part of our codebase, so we need your permission to use and distribute your code. We also need to be sure of various other things—for instance that you'll tell us if you know that your code infringes on other people's patents. You don't have to sign the CLA until after you've submitted your code for review and a member has approved it, but you must do it before we can put your code into our codebase. Before you start working on a larger contribution, you should get in touch with us first through the issue tracker with your idea so that we can help out and possibly guide you. Coordinating up front makes it much easier to avoid frustration later on. ### Code reviews All submissions, including submissions by project members, require review. We use Github pull requests for this purpose. ### The small print Contributions made by corporations are covered by a different agreement than the one above, the [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). glyphslib-2.2.1/LICENSE000066400000000000000000000261351322341616200145250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. glyphslib-2.2.1/Lib/000077500000000000000000000000001322341616200142175ustar00rootroot00000000000000glyphslib-2.2.1/Lib/glyphsLib/000077500000000000000000000000001322341616200161545ustar00rootroot00000000000000glyphslib-2.2.1/Lib/glyphsLib/__init__.py000066400000000000000000000102131322341616200202620ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from io import open import logging from fontTools.misc.py23 import tostr from glyphsLib.builder import to_ufos from glyphsLib.interpolation import interpolate, build_designspace from glyphsLib.parser import load, loads from glyphsLib.writer import dump, dumps from glyphsLib.util import write_ufo from glyphsLib.classes import __all__ as __all_classes__ from glyphsLib.classes import * __version__ = "2.2.1" # Doing `import *` from a module that uses unicode_literals, produces # "TypeError: Item in ``from list'' must be str, not unicode" on Python 2. # Thus we need to encode the unicode literals as ascii bytes. # https://bugs.python.org/issue21720 __all__ = [tostr(s) for s in [ "build_masters", "build_instances", "load_to_ufos", "load", "loads", "dump", "dumps", ] + __all_classes__] logger = logging.getLogger(__name__) def load_to_ufos(file_or_path, include_instances=False, family_name=None, propagate_anchors=True): """Load an unpacked .glyphs object to UFO objects.""" if hasattr(file_or_path, 'read'): font = load(file_or_path) else: with open(file_or_path, 'r', encoding='utf-8') as ifile: font = load(ifile) logger.info('Loading to UFOs') return to_ufos(font, include_instances=include_instances, family_name=family_name, propagate_anchors=propagate_anchors) def build_masters(filename, master_dir, designspace_instance_dir=None, family_name=None, propagate_anchors=True): """Write and return UFOs from the masters defined in a .glyphs file. Args: master_dir: Directory where masters are written. designspace_instance_dir: If provided, a designspace document will be written alongside the master UFOs though no instances will be built. family_name: If provided, the master UFOs will be given this name and only instances with this name will be included in the designspace. Returns: A list of master UFOs, and if designspace_instance_dir is provided, a path to a designspace and a list of (path, data) tuples with instance paths from the designspace and respective data from the Glyphs source. """ ufos, instance_data = load_to_ufos( filename, include_instances=True, family_name=family_name, propagate_anchors=propagate_anchors) if designspace_instance_dir is not None: designspace_path, instance_data = build_designspace( ufos, master_dir, designspace_instance_dir, instance_data) return ufos, designspace_path, instance_data else: for ufo in ufos: write_ufo(ufo, master_dir) return ufos def build_instances(filename, master_dir, instance_dir, family_name=None, propagate_anchors=True, round_geometry=True): """Write and return UFOs from the instances defined in a .glyphs file. Args: master_dir: Directory where masters are written. instance_dir: Directory where instances are written. family_name: If provided, the master UFOs will be given this name and only instances with this name will be built. """ master_ufos, instance_data = load_to_ufos( filename, include_instances=True, family_name=family_name, propagate_anchors=propagate_anchors) instance_ufos = interpolate( master_ufos, master_dir, instance_dir, instance_data, round_geometry=round_geometry) return instance_ufos glyphslib-2.2.1/Lib/glyphsLib/__main__.py000066400000000000000000000044231322341616200202510ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division, absolute_import, unicode_literals import sys import argparse import glyphsLib description = """\n Converts a Glyphs.app source file into UFO masters or UFO instances and MutatorMath designspace. """ def parse_options(args): parser = argparse.ArgumentParser(description=description) parser.add_argument("--version", action="version", version='glyphsLib %s' % (glyphsLib.__version__)) parser.add_argument("-g", "--glyphs", metavar="GLYPHS", required=True, help="Glyphs file to convert.") parser.add_argument("-m", "--masters", metavar="MASTERS", default="master_ufo", help="Ouput masters UFO to folder MASTERS. " "(default: %(default)s)") parser.add_argument("-n", "--instances", metavar="INSTANCES", nargs="?", const="instance_ufo", default=None, help="Output and generate interpolated instances UFO " "to folder INSTANCES. " "(default: %(const)s)") parser.add_argument("-R", "--no-round", action="store_false", help="Round geometry to integers") options = parser.parse_args(args) return options def main(args=None): opt = parse_options(args) if opt.glyphs is not None: if opt.instances is None: glyphsLib.build_masters(opt.glyphs, opt.masters) else: glyphsLib.build_instances(opt.glyphs, opt.masters, opt.instances, round_geometry=opt.no_round) if __name__ == '__main__': main(sys.argv[1:]) glyphslib-2.2.1/Lib/glyphsLib/affine/000077500000000000000000000000001322341616200174045ustar00rootroot00000000000000glyphslib-2.2.1/Lib/glyphsLib/affine/__init__.py000066400000000000000000000346251322341616200215270ustar00rootroot00000000000000"""Affine transformation matrices The 3x3 augmented affine transformation matrix for transformations in two dimensions is illustrated below. | x' | | a b c | | x | | y' | = | d e f | | y | | 1 | | 0 0 1 | | 1 | The Affine package is derived from Casey Duncan's Planar package. See the copyright statement below. """ ############################################################################# # Copyright (c) 2010 by Casey Duncan # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name(s) of the copyright holders nor the names of its # contributors may be used to endorse or promote products derived from this # software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AS IS AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO # EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ############################################################################# from __future__ import division from collections import namedtuple import math __all__ = ['Affine'] __author__ = "Sean Gillies" __version__ = "1.0" EPSILON=1e-5 EPSILON2=EPSILON**2 def set_epsilon(epsilon): """Set the global absolute error value and rounding limit for approximate floating point comparison operations. This value is accessible via the :attr:`planar.EPSILON` global variable. The default value of ``0.00001`` is suitable for values that are in the "countable range". You may need a larger epsilon when using large absolute values, and a smaller value for very small values close to zero. Otherwise approximate comparison operations will not behave as expected. """ EPSILON = float(epsilon) EPSILON2 = EPSILON**2 class TransformNotInvertibleError(Exception): """The transform could not be inverted""" # Define assert_unorderable() depending on the language # implicit ordering rules. This keeps things consistent # across major Python versions try: 3 > "" except TypeError: # pragma: no cover # No implicit ordering (newer Python) def assert_unorderable(a, b): """Assert that a and b are unorderable""" return NotImplemented else: # pragma: no cover # Implicit ordering by default (older Python) # We must raise an exception ourselves # To prevent nonsensical ordering def assert_unorderable(a, b): """Assert that a and b are unorderable""" raise TypeError("unorderable types: %s and %s" % (type(a).__name__, type(b).__name__)) def cached_property(func): """Special property decorator that caches the computed property value in the object's instance dict the first time it is accessed. """ name = func.__name__ doc = func.__doc__ def getter(self, name=name): try: return self.__dict__[name] except KeyError: self.__dict__[name] = value = func(self) return value getter.func_name = name return property(getter, doc=doc) def cos_sin_deg(deg): """Return the cosine and sin for the given angle in degrees, with special-case handling of multiples of 90 for perfect right angles """ deg = deg % 360.0 if deg == 90.0: return 0.0, 1.0 elif deg == 180.0: return -1.0, 0 elif deg == 270.0: return 0, -1.0 rad = math.radians(deg) return math.cos(rad), math.sin(rad) class Affine( namedtuple('Affine', ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'))): """Two dimensional affine transform for linear mapping from 2D coordinates to other 2D coordinates. Parallel lines are preserved by these transforms. Affine transforms can perform any combination of translations, scales/flips, shears, and rotations. Class methods are provided to conveniently compose transforms from these operations. Internally the transform is stored as a 3x3 transformation matrix. The transform may be constructed directly by specifying the first two rows of matrix values as 6 floats. Since the matrix is an affine transform, the last row is always ``(0, 0, 1)``. :param members: 6 floats for the first two matrix rows. :type members: float """ def __new__(self, *members): if len(members) == 6: mat3x3 = [x * 1.0 for x in members] + [0.0, 0.0, 1.0] return tuple.__new__(Affine, mat3x3) else: raise TypeError( "Expected 6 number args, got %s" % len(members)) @classmethod def from_gdal(cls, c, a, b, f, d, e): members = [a, b, c, d, e, f] mat3x3 = [x * 1.0 for x in members] + [0.0, 0.0, 1.0] return tuple.__new__(Affine, mat3x3) @classmethod def identity(cls): """Return the identity transform. :rtype: Affine """ return identity @classmethod def translation(cls, xoff, yoff): """Create a translation transform from an offset vector. :param xoff: Translation x offset. :type xoff: float :param yoff: Translation y offset. :type yoff: float :rtype: Affine """ return tuple.__new__(cls, (1.0, 0.0, xoff, 0.0, 1.0, yoff, 0.0, 0.0, 1.0)) @classmethod def scale(cls, *scaling): """Create a scaling transform from a scalar or vector. :param scaling: The scaling factor. A scalar value will scale in both dimensions equally. A vector scaling value scales the dimensions independently. :type scaling: float or sequence :rtype: Affine """ if len(scaling) == 1: sx = sy = float(scaling[0]) else: sx, sy = scaling return tuple.__new__(cls, (sx, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 1.0)) @classmethod def shear(cls, x_angle=0, y_angle=0): """Create a shear transform along one or both axes. :param x_angle: Angle in degrees to shear along the x-axis. :type x_angle: float :param y_angle: Angle in degrees to shear along the y-axis. :type y_angle: float :rtype: Affine """ sx = math.tan(math.radians(x_angle)) sy = math.tan(math.radians(y_angle)) return tuple.__new__(cls, (1.0, sy, 0.0, sx, 1.0, 0.0, 0.0, 0.0, 1.0)) @classmethod def rotation(cls, angle, pivot=None): """Create a rotation transform at the specified angle, optionally about the specified pivot point. :param angle: Rotation angle in degrees :type angle: float :param pivot: Point to rotate about, if omitted the rotation is about the origin. :type pivot: sequence :rtype: Affine """ ca, sa = cos_sin_deg(angle) if pivot is None: return tuple.__new__(cls, (ca, sa, 0.0, -sa, ca, 0.0, 0.0, 0.0, 1.0)) else: px, py = pivot return tuple.__new__(cls, (ca, sa, px - px*ca + py*sa, -sa, ca, py - px*sa - py*ca, 0.0, 0.0, 1.0)) def __str__(self): """Concise string representation.""" return ("|% .2f,% .2f,% .2f|\n" "|% .2f,% .2f,% .2f|\n" "|% .2f,% .2f,% .2f|") % self def __repr__(self): """Precise string representation.""" return ("Affine(%r, %r, %r,\n" " %r, %r, %r)") % self[:6] def to_gdal(self): return (self.c, self.a, self.b, self.f, self.d, self.e) @property def xoff(self): return self.c @property def yoff(self): return self.f @cached_property def determinant(self): """The determinant of the transform matrix. This value is equal to the area scaling factor when the transform is applied to a shape. """ a, b, c, d, e, f, g, h, i = self return a*e - b*d @cached_property def is_identity(self): """True if this transform equals the identity matrix, within rounding limits. """ return self is identity or self.almost_equals(identity) @cached_property def is_rectilinear(self): """True if the transform is rectilinear, i.e., whether a shape would remain axis-aligned, within rounding limits, after applying the transform. """ a, b, c, d, e, f, g, h, i = self return ((abs(a) < EPSILON and abs(e) < EPSILON) or (abs(d) < EPSILON and abs(b) < EPSILON)) @cached_property def is_conformal(self): """True if the transform is conformal, i.e., if angles between points are preserved after applying the transform, within rounding limits. This implies that the transform has no effective shear. """ a, b, c, d, e, f, g, h, i = self return abs(a*b + d*e) < EPSILON @cached_property def is_orthonormal(self): """True if the transform is orthonormal, which means that the transform represents a rigid motion, which has no effective scaling or shear. Mathematically, this means that the axis vectors of the transform matrix are perpendicular and unit-length. Applying an orthonormal transform to a shape always results in a congruent shape. """ a, b, c, d, e, f, g, h, i = self return (self.is_conformal and abs(1.0 - (a*a + d*d)) < EPSILON and abs(1.0 - (b*b + e*e)) < EPSILON) @cached_property def is_degenerate(self): """True if this transform is degenerate, which means that it will collapse a shape to an effective area of zero. Degenerate transforms cannot be inverted. """ return abs(self.determinant) < EPSILON @property def column_vectors(self): """The values of the transform as three 2D column vectors""" a, b, c, d, e, f, _, _, _ = self return (a, d), (b, e), (c, f) def almost_equals(self, other): """Compare transforms for approximate equality. :param other: Transform being compared. :type other: Affine :return: True if absolute difference between each element of each respective tranform matrix < ``EPSILON``. """ for i in (0, 1, 2, 3, 4, 5): if abs(self[i] - other[i]) >= EPSILON: return False return True def __gt__(self, other): return assert_unorderable(self, other) __ge__ = __lt__ = __le__ = __gt__ # Override from base class. We do not support entrywise # addition, subtraction or scalar multiplication because # the result is not an affine transform def __add__(self, other): raise TypeError("Operation not supported") __iadd__ = __add__ def __mul__(self, other): """Apply the transform using matrix multiplication, creating a resulting object of the same type. A transform may be applied to another transform, a vector, vector array, or shape. :param other: The object to transform. :type other: Affine, :class:`~planar.Vec2`, :class:`~planar.Vec2Array`, :class:`~planar.Shape` :rtype: Same as ``other`` """ sa, sb, sc, sd, se, sf, _, _, _ = self if isinstance(other, Affine): oa, ob, oc, od, oe, of, _, _, _ = other return tuple.__new__(Affine, (sa*oa + sb*od, sa*ob + sb*oe, sa*oc + sb*of + sc, sd*oa + se*od, sd*ob + se*oe, sd*oc + se*of + sf, 0.0, 0.0, 1.0)) else: try: vx, vy = other except Exception: return NotImplemented return (vx*sa + vy*sd + sc, vx*sb + vy*se + sf) def __rmul__(self, other): # We should not be called if other is an affine instance # This is just a guarantee, since we would potentially # return the wrong answer in that case assert not isinstance(other, Affine) return self.__mul__(other) def __imul__(self, other): if isinstance(other, Affine) or isinstance(other, tuple): return self.__mul__(other) else: return NotImplemented def itransform(self, seq): """Transform a sequence of points or vectors in place. :param seq: Mutable sequence of :class:`~planar.Vec2` to be transformed. :returns: None, the input sequence is mutated in place. """ if self is not identity and self != identity: sa, sb, sc, sd, se, sf, _, _, _ = self for i, (x, y) in enumerate(seq): seq[i] = (x*sa + y*sd + sc, x*sb + y*se + sf) def __invert__(self): """Return the inverse transform. :raises: :except:`TransformNotInvertible` if the transform is degenerate. """ if self.is_degenerate: raise TransformNotInvertibleError( "Cannot invert degenerate transform") idet = 1.0 / self.determinant sa, sb, sc, sd, se, sf, _, _, _ = self ra = se * idet rb = -sb * idet rd = -sd * idet re = sa * idet return tuple.__new__(Affine, (ra, rb, -sc*ra - sf*rb, rd, re, -sc*rd - sf*re, 0.0, 0.0, 1.0)) __hash__ = tuple.__hash__ # hash is not inherited in Py 3 identity = Affine(1, 0, 0, 0, 1, 0) """The identity transform""" # vim: ai ts=4 sts=4 et sw=4 tw=78 glyphslib-2.2.1/Lib/glyphsLib/builder/000077500000000000000000000000001322341616200176025ustar00rootroot00000000000000glyphslib-2.2.1/Lib/glyphsLib/builder/__init__.py000066400000000000000000000035341322341616200217200ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging from glyphsLib import classes import defcon from .builders import UFOBuilder, GlyphsBuilder logger = logging.getLogger(__name__) def to_ufos(font, include_instances=False, family_name=None, propagate_anchors=True, ufo_module=defcon): """Take .glyphs file data and load it into UFOs. Takes in data as Glyphs.app-compatible classes, as documented at https://docu.glyphsapp.com/ If include_instances is True, also returns the parsed instance data. If family_name is provided, the master UFOs will be given this name and only instances with this name will be returned. """ builder = UFOBuilder( font, ufo_module=ufo_module, family_name=family_name, propagate_anchors=propagate_anchors) result = list(builder.masters) if include_instances: return result, builder.instance_data return result def to_glyphs(ufos, designspace=None, glyphs_module=classes): """ Take a list of UFOs and combine them into a single .glyphs file. This should be the inverse function of `to_ufos`, so we should have to_glyphs(to_ufos(font)) == font """ builder = GlyphsBuilder( ufos, designspace=designspace, glyphs_module=glyphs_module) return builder.font glyphslib-2.2.1/Lib/glyphsLib/builder/anchors.py000066400000000000000000000075351322341616200216230ustar00rootroot00000000000000# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from fontTools.misc.transform import Transform __all__ = ['to_ufo_propagate_font_anchors'] def to_ufo_propagate_font_anchors(self, ufo): """Copy anchors from parent glyphs' components to the parent.""" processed = set() for glyph in ufo: _propagate_glyph_anchors(ufo, glyph, processed) def _propagate_glyph_anchors(ufo, parent, processed): """Propagate anchors for a single parent glyph.""" if parent.name in processed: return processed.add(parent.name) base_components = [] mark_components = [] anchor_names = set() to_add = {} for component in parent.components: glyph = ufo[component.baseGlyph] _propagate_glyph_anchors(ufo, glyph, processed) if any(a.name.startswith('_') for a in glyph.anchors): mark_components.append(component) else: base_components.append(component) anchor_names |= {a.name for a in glyph.anchors} for anchor_name in anchor_names: # don't add if parent already contains this anchor OR any associated # ligature anchors (e.g. "top_1, top_2" for "top") if not any(a.name.startswith(anchor_name) for a in parent.anchors): _get_anchor_data(to_add, ufo, base_components, anchor_name) for component in mark_components: _adjust_anchors(to_add, ufo, component) # we sort propagated anchors to append in a deterministic order for name, (x, y) in sorted(to_add.items()): anchor_dict = {'name': name, 'x': x, 'y': y} parent.appendAnchor(glyph.anchorClass(anchorDict=anchor_dict)) def _get_anchor_data(anchor_data, ufo, components, anchor_name): """Get data for an anchor from a list of components.""" anchors = [] for component in components: for anchor in ufo[component.baseGlyph].anchors: if anchor.name == anchor_name: anchors.append((anchor, component)) break if len(anchors) > 1: for i, (anchor, component) in enumerate(anchors): t = Transform(*component.transformation) name = '%s_%d' % (anchor.name, i + 1) anchor_data[name] = t.transformPoint((anchor.x, anchor.y)) elif anchors: anchor, component = anchors[0] t = Transform(*component.transformation) anchor_data[anchor.name] = t.transformPoint((anchor.x, anchor.y)) def _adjust_anchors(anchor_data, ufo, component): """Adjust anchors to which a mark component may have been attached.""" glyph = ufo[component.baseGlyph] t = Transform(*component.transformation) for anchor in glyph.anchors: # only adjust if this anchor has data and the component also contains # the associated mark anchor (e.g. "_top" for "top") if (anchor.name in anchor_data and any(a.name == '_' + anchor.name for a in glyph.anchors)): anchor_data[anchor.name] = t.transformPoint((anchor.x, anchor.y)) def to_ufo_glyph_anchors(self, glyph, anchors): """Add .glyphs anchors to a glyph.""" for anchor in anchors: x, y = anchor.position anchor_dict = {'name': anchor.name, 'x': x, 'y': y} glyph.appendAnchor(anchor_dict) glyphslib-2.2.1/Lib/glyphsLib/builder/blue_values.py000066400000000000000000000023741322341616200224700ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) def to_ufo_blue_values(self, ufo, master): """Set postscript blue values from Glyphs alignment zones.""" alignment_zones = master.alignmentZones blue_values = [] other_blues = [] for zone in sorted(alignment_zones): pos = zone.position size = zone.size val_list = blue_values if pos == 0 or size >= 0 else other_blues val_list.extend(sorted((pos, pos + size))) ufo.info.postscriptBlueValues = blue_values ufo.info.postscriptOtherBlues = other_blues def to_glyphs_blue_values(self, ufo, master): pass glyphslib-2.2.1/Lib/glyphsLib/builder/builders.py000066400000000000000000000274221322341616200217740ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from collections import OrderedDict import logging import defcon from glyphsLib import classes from .constants import PUBLIC_PREFIX, GLYPHS_PREFIX class _LoggerMixin(object): _logger = None @property def logger(self): if self._logger is None: self._logger = logging.getLogger( ".".join([self.__class__.__module__, self.__class__.__name__])) return self._logger class UFOBuilder(_LoggerMixin): """Builder for Glyphs to UFO + designspace.""" def __init__(self, font, ufo_module=defcon, family_name=None, propagate_anchors=True): """Create a builder that goes from Glyphs to UFO + designspace. Keyword arguments: font -- The GSFont object to transform into UFOs ufo_module -- A Python module to use to build UFO objects (you can pass a custom module that has the same classes as the official defcon to get instances of your own classes) family_name -- if provided, the master UFOs will be given this name and only instances with this name will be returned. propagate_anchors -- set to False to prevent anchor propagation """ self.font = font self.ufo_module = ufo_module # The set of UFOs (= defcon.Font objects) that will be built, # indexed by master ID, the same order as masters in the source GSFont. self._ufos = OrderedDict() # The MutatorMath Designspace object that will be built (if requested). self._designspace = None # check that source was generated with at least stable version 2.3 # https://github.com/googlei18n/glyphsLib/pull/65#issuecomment-237158140 if int(font.appVersion) < 895: self.logger.warn( 'This Glyphs source was generated with an outdated version ' 'of Glyphs. The resulting UFOs may be incorrect.') source_family_name = self.font.familyName if family_name is None: # use the source family name, and include all the instances self.family_name = source_family_name self._do_filter_instances_by_family = False else: self.family_name = family_name # use a custom 'family_name' to name master UFOs, and only build # instances with matching 'familyName' custom parameter self._do_filter_instances_by_family = True if family_name == source_family_name: # if the 'family_name' provided is the same as the source, only # include instances which do _not_ specify a custom 'familyName' self._instance_family_name = None else: self._instance_family_name = family_name self.propagate_anchors = propagate_anchors @property def masters(self): """Get an iterator over master UFOs that match the given family_name. """ if self._ufos: return self._ufos.values() kerning_groups = {} # Store set of actually existing master (layer) ids. This helps with # catching dangling layer data that Glyphs may ignore, e.g. when # copying glyphs from other fonts with, naturally, different master # ids. Note: Masters have unique ids according to the Glyphs # documentation and can therefore be stored in a set. master_layer_ids = {m.id for m in self.font.masters} # stores background data from "associated layers" supplementary_layer_data = [] # TODO(jamesgk) maybe create one font at a time to reduce memory usage # TODO: (jany) in the future, return a lazy iterator that builds UFOs # on demand. self.to_ufo_font_attributes(self.family_name) # get the 'glyphOrder' custom parameter as stored in the lib.plist. # We assume it's the same for all ufos. first_ufo = next(iter(self._ufos.values())) glyphOrder_key = PUBLIC_PREFIX + 'glyphOrder' if glyphOrder_key in first_ufo.lib: glyph_order = first_ufo.lib[glyphOrder_key] else: glyph_order = [] sorted_glyphset = set(glyph_order) for glyph in self.font.glyphs: self.to_ufo_glyph_groups(kerning_groups, glyph) glyph_name = glyph.name if glyph_name not in sorted_glyphset: # glyphs not listed in the 'glyphOrder' custom parameter but still # in the font are appended after the listed glyphs, in the order # in which they appear in the source file glyph_order.append(glyph_name) for layer in glyph.layers.values(): layer_id = layer.layerId layer_name = layer.name assoc_id = layer.associatedMasterId if assoc_id != layer.layerId: # Store all layers, even the invalid ones, and just skip # them and print a warning below. supplementary_layer_data.append( (assoc_id, glyph_name, layer_name, layer)) continue ufo = self._ufos[layer_id] ufo_glyph = ufo.newGlyph(glyph_name) self.to_ufo_glyph(ufo_glyph, layer, glyph) for layer_id, glyph_name, layer_name, layer_data \ in supplementary_layer_data: if (layer_data.layerId not in master_layer_ids and layer_data.associatedMasterId not in master_layer_ids): self.logger.warn( '{}, glyph "{}": Layer "{}" is dangling and will be ' 'skipped. Did you copy a glyph from a different font? If ' 'so, you should clean up any phantom layers not associated ' 'with an actual master.'.format( self.font.familyName, glyph_name, layer_data.layerId)) continue if not layer_name: # Empty layer names are invalid according to the UFO spec. self.logger.warn( '{}, glyph "{}": Contains layer without a name which will ' 'be skipped.'.format(self.font.familyName, glyph_name)) continue ufo_font = self._ufos[layer_id] if layer_name not in ufo_font.layers: ufo_layer = ufo_font.newLayer(layer_name) else: ufo_layer = ufo_font.layers[layer_name] ufo_glyph = ufo_layer.newGlyph(glyph_name) self.to_ufo_glyph(ufo_glyph, layer_data, layer_data.parent) for ufo in self._ufos.values(): ufo.lib[glyphOrder_key] = glyph_order if self.propagate_anchors: self.to_ufo_propagate_font_anchors(ufo) self.to_ufo_features(ufo) self.to_ufo_kerning_groups(ufo, kerning_groups) for master_id, kerning in self.font.kerning.items(): self.to_ufo_kerning(self._ufos[master_id], kerning) return self._ufos.values() @property def instances(self): """Get an iterator over interpolated UFOs of instances.""" # TODO? return [] @property def designspace(self): """Get a designspace Document instance that links the masters together. """ # TODO? pass @property def instance_data(self): instances = self.font.instances if self._do_filter_instances_by_family: instances = list( filter_instances_by_family(instances, self._instance_family_name)) instance_data = {'data': instances} first_ufo = next(iter(self.masters)) # the 'Variation Font Origin' is a font-wide custom parameter, thus it is # shared by all the master ufos; here we just get it from the first one varfont_origin_key = "Variation Font Origin" varfont_origin = first_ufo.lib.get(GLYPHS_PREFIX + varfont_origin_key) if varfont_origin: instance_data[varfont_origin_key] = varfont_origin return instance_data # Implementation is spit into one file per feature from .anchors import to_ufo_propagate_font_anchors, to_ufo_glyph_anchors from .blue_values import to_ufo_blue_values from .common import to_ufo_time from .components import to_ufo_draw_components from .custom_params import to_ufo_custom_params from .features import to_ufo_features from .font import to_ufo_font_attributes from .glyph import (to_ufo_glyph, to_ufo_glyph_background, to_ufo_glyph_libdata) from .guidelines import to_ufo_guidelines from .kerning import (to_ufo_kerning, to_ufo_glyph_groups, to_ufo_kerning_groups) from .names import to_ufo_names from .paths import to_ufo_draw_paths from .user_data import to_ufo_family_user_data, to_ufo_master_user_data def filter_instances_by_family(instances, family_name=None): """Yield instances whose 'familyName' custom parameter is equal to 'family_name'. """ for instance in instances: familyName = None for p in instance.customParameters: param, value = p.name, p.value if param == 'familyName': familyName = value if familyName == family_name: yield instance class GlyphsBuilder(_LoggerMixin): """Builder for UFO + designspace to Glyphs.""" def __init__(self, ufos, designspace=None, glyphs_module=classes): """Create a builder that goes from UFOs + designspace to Glyphs. Keyword arguments: ufos -- The list of UFOs to combine into a GSFont designspace -- A MutatorMath Designspace to use for the GSFont glyphs_module -- The glyphsLib.classes module to use to build glyphsLib classes (you can pass a custom module with the same classes as the official glyphsLib.classes to get instances of your own classes, or pass the Glyphs.app module that holds the official classes to import UFOs into Glyphs.app) """ self.ufos = ufos self.designspace = designspace self.glyphs_module = glyphs_module self._font = None """The GSFont that will be built.""" @property def font(self): """Get the GSFont built from the UFOs + designspace.""" if self._font is not None: return self._font self._font = self.glyphs_module.GSFont() for index, ufo in enumerate(self.ufos): master = self.glyphs_module.GSFontMaster() self.to_glyphs_font_attributes(ufo, master, is_initial=(index == 0)) self._font.masters.insert(len(self._font.masters), master) # TODO: all the other stuff! return self._font # Implementation is spit into one file per feature from .font import to_glyphs_font_attributes from .blue_values import to_glyphs_blue_values glyphslib-2.2.1/Lib/glyphsLib/builder/common.py000066400000000000000000000015541322341616200214510ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) UFO_FORMAT = '%Y/%m/%d %H:%M:%S' def to_ufo_time(datetime_obj): """Format a datetime object as specified for UFOs.""" return datetime_obj.strftime(UFO_FORMAT) glyphslib-2.2.1/Lib/glyphsLib/builder/components.py000066400000000000000000000016771322341616200223540ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) def to_ufo_draw_components(self, pen, components): """Draw .glyphs components onto a pen, adding them to the parent glyph.""" for component in components: pen.addComponent(component.name, component.transform) glyphslib-2.2.1/Lib/glyphsLib/builder/constants.py000066400000000000000000000037251322341616200221770ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) PUBLIC_PREFIX = 'public.' GLYPHS_PREFIX = 'com.schriftgestaltung.' GLYPHLIB_PREFIX = GLYPHS_PREFIX + 'Glyphs.' ROBOFONT_PREFIX = 'com.typemytype.robofont.' UFO2FT_FILTERS_KEY = 'com.github.googlei18n.ufo2ft.filters' UFO2FT_USE_PROD_NAMES_KEY = 'com.github.googlei18n.ufo2ft.useProductionNames' GLYPHS_COLORS = ( '0.85,0.26,0.06,1', '0.99,0.62,0.11,1', '0.65,0.48,0.2,1', '0.97,1,0,1', '0.67,0.95,0.38,1', '0.04,0.57,0.04,1', '0,0.67,0.91,1', '0.18,0.16,0.78,1', '0.5,0.09,0.79,1', '0.98,0.36,0.67,1', '0.75,0.75,0.75,1', '0.25,0.25,0.25,1') # https://www.microsoft.com/typography/otspec/os2.htm#cpr CODEPAGE_RANGES = { 1252: 0, 1250: 1, 1251: 2, 1253: 3, 1254: 4, 1255: 5, 1256: 6, 1257: 7, 1258: 8, # 9-15: Reserved for Alternate ANSI 874: 16, 932: 17, 936: 18, 949: 19, 950: 20, 1361: 21, # 22-28: Reserved for Alternate ANSI and OEM # 29: Macintosh Character Set (US Roman) # 30: OEM Character Set # 31: Symbol Character Set # 32-47: Reserved for OEM 869: 48, 866: 49, 865: 50, 864: 51, 863: 52, 862: 53, 861: 54, 860: 55, 857: 56, 855: 57, 852: 58, 775: 59, 737: 60, 708: 61, 850: 62, 437: 63, } glyphslib-2.2.1/Lib/glyphsLib/builder/custom_params.py000066400000000000000000000202271322341616200230340ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) import re from glyphsLib.util import bin_to_int_list from .filters import parse_glyphs_filter from .constants import (GLYPHS_PREFIX, PUBLIC_PREFIX, CODEPAGE_RANGES, UFO2FT_FILTERS_KEY, UFO2FT_USE_PROD_NAMES_KEY) from .features import replace_feature def to_ufo_custom_params(self, ufo, master): misc = ['DisplayStrings', 'disablesAutomaticAlignment', 'disablesNiceNames'] custom_params = parse_custom_params(self.font, misc) set_custom_params(ufo, parsed=custom_params) # the misc attributes double as deprecated info attributes! # they are Glyphs-related, not OpenType-related, and don't go in info misc = ('customValue', 'weightValue', 'widthValue') set_custom_params(ufo, data=master, misc_keys=misc, non_info=misc) set_default_params(ufo) def set_custom_params(ufo, parsed=None, data=None, misc_keys=(), non_info=()): """Set Glyphs custom parameters in UFO info or lib, where appropriate. Custom parameter data can be pre-parsed out of Glyphs data and provided via the `parsed` argument, otherwise `data` should be provided and will be parsed. The `parsed` option is provided so that custom params can be popped from Glyphs data once and used several times; in general this is used for debugging purposes (to detect unused Glyphs data). The `non_info` argument can be used to specify potential UFO info attributes which should not be put in UFO info. """ if parsed is None: parsed = parse_custom_params(data or {}, misc_keys) else: assert data is None, "Shouldn't provide parsed data and data to parse." fsSelection_flags = {'Use Typo Metrics', 'Has WWS Names'} for name, value in parsed: name = normalize_custom_param_name(name) if name == "Don't use Production Names": # convert between Glyphs.app's and ufo2ft's equivalent parameter ufo.lib[UFO2FT_USE_PROD_NAMES_KEY] = not value continue if name in fsSelection_flags: if value: if ufo.info.openTypeOS2Selection is None: ufo.info.openTypeOS2Selection = [] if name == 'Use Typo Metrics': ufo.info.openTypeOS2Selection.append(7) elif name == 'Has WWS Names': ufo.info.openTypeOS2Selection.append(8) continue # deal with any Glyphs naming quirks here if name == 'disablesNiceNames': name = 'useNiceNames' value = not value if name == 'Disable Last Change': name = 'disablesLastChange' # convert code page numbers to OS/2 ulCodePageRange bits if name == 'codePageRanges': value = [CODEPAGE_RANGES[v] for v in value] # convert Glyphs' GASP Table to UFO openTypeGaspRangeRecords if name == 'GASP Table': name = 'openTypeGaspRangeRecords' # XXX maybe the parser should cast the gasp values to int? value = {int(k): int(v) for k, v in value.items()} gasp_records = [] # gasp range records must be sorted in ascending rangeMaxPPEM for max_ppem, gasp_behavior in sorted(value.items()): gasp_records.append({ 'rangeMaxPPEM': max_ppem, 'rangeGaspBehavior': bin_to_int_list(gasp_behavior)}) value = gasp_records opentype_attr_prefix_pairs = ( ('hhea', 'Hhea'), ('description', 'NameDescription'), ('license', 'NameLicense'), ('licenseURL', 'NameLicenseURL'), ('preferredFamilyName', 'NamePreferredFamilyName'), ('preferredSubfamilyName', 'NamePreferredSubfamilyName'), ('compatibleFullName', 'NameCompatibleFullName'), ('sampleText', 'NameSampleText'), ('WWSFamilyName', 'NameWWSFamilyName'), ('WWSSubfamilyName', 'NameWWSSubfamilyName'), ('panose', 'OS2Panose'), ('typo', 'OS2Typo'), ('unicodeRanges', 'OS2UnicodeRanges'), ('codePageRanges', 'OS2CodePageRanges'), ('weightClass', 'OS2WeightClass'), ('widthClass', 'OS2WidthClass'), ('win', 'OS2Win'), ('vendorID', 'OS2VendorID'), ('versionString', 'NameVersion'), ('fsType', 'OS2Type')) for glyphs_prefix, ufo_prefix in opentype_attr_prefix_pairs: name = re.sub( '^' + glyphs_prefix, 'openType' + ufo_prefix, name) postscript_attrs = ('underlinePosition', 'underlineThickness') if name in postscript_attrs: name = 'postscript' + name[0].upper() + name[1:] # enforce that winAscent/Descent are positive, according to UFO spec if name.startswith('openTypeOS2Win') and value < 0: value = -value # The value of these could be a float or str, and UFO expects an int. if name in ('openTypeOS2WeightClass', 'openTypeOS2WidthClass', 'xHeight'): value = int(value) if name == 'glyphOrder': # store the public.glyphOrder in lib.plist ufo.lib[PUBLIC_PREFIX + name] = value elif name in ('PreFilter', 'Filter'): filter_struct = parse_glyphs_filter( value, is_pre=name.startswith('Pre')) if not filter_struct: continue if UFO2FT_FILTERS_KEY not in ufo.lib.keys(): ufo.lib[UFO2FT_FILTERS_KEY] = [] ufo.lib[UFO2FT_FILTERS_KEY].append(filter_struct) elif name == "Replace Feature": tag, repl = re.split("\s*;\s*", value, 1) ufo.features.text = replace_feature(tag, repl, ufo.features.text or "") elif hasattr(ufo.info, name) and name not in non_info: # most OpenType table entries go in the info object setattr(ufo.info, name, value) else: # everything else gets dumped in the lib ufo.lib[GLYPHS_PREFIX + name] = value def set_default_params(ufo): """ Set Glyphs.app's default parameters when different from ufo2ft ones. """ # ufo2ft defaults to fsType Bit 2 ("Preview & Print embedding"), while # Glyphs.app defaults to Bit 3 ("Editable embedding") if ufo.info.openTypeOS2Type is None: ufo.info.openTypeOS2Type = [3] # Reference: # https://glyphsapp.com/content/1-get-started/2-manuals/1-handbook-glyphs-2-0/Glyphs-Handbook-2.3.pdf#page=200 if ufo.info.postscriptUnderlineThickness is None: ufo.info.postscriptUnderlineThickness = 50 if ufo.info.postscriptUnderlinePosition is None: ufo.info.postscriptUnderlinePosition = -100 def normalize_custom_param_name(name): """Replace curved quotes with straight quotes in a custom parameter name. These should be the only keys with problematic (non-ascii) characters, since they can be user-generated. """ replacements = ( (u'\u2018', "'"), (u'\u2019', "'"), (u'\u201C', '"'), (u'\u201D', '"')) for orig, replacement in replacements: name = name.replace(orig, replacement) return name def parse_custom_params(font, misc_keys): """Parse customParameters into a list of pairs.""" params = [] for p in font.customParameters: params.append((p.name, p.value)) for key in misc_keys: try: val = getattr(font, key) except AttributeError: continue if val is not None: params.append((key, val)) return params glyphslib-2.2.1/Lib/glyphsLib/builder/features.py000066400000000000000000000130601322341616200217720ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from fontTools.misc.py23 import round, unicode import re import glyphsLib from .constants import GLYPHLIB_PREFIX, PUBLIC_PREFIX def autostr(automatic): return '# automatic\n' if automatic else '' def to_ufo_features(self, ufo): """Write an UFO's OpenType feature file.""" prefix_str = '\n\n'.join('# Prefix: %s\n%s%s' % (prefix.name, autostr(prefix.automatic), prefix.code.strip()) for prefix in self.font.featurePrefixes) class_defs = [] for class_ in self.font.classes: prefix = '@' if not class_.name.startswith('@') else '' name = prefix + class_.name class_defs.append('%s%s = [ %s ];' % (autostr(class_.automatic), name, class_.code)) class_str = '\n\n'.join(class_defs) feature_defs = [] for feature in self.font.features: code = feature.code.strip() lines = ['feature %s {' % feature.name] if feature.notes: lines.append('# notes:') lines.extend('# ' + line for line in feature.notes.splitlines()) if feature.automatic: lines.append('# automatic') if feature.disabled: lines.append('# disabled') lines.extend('#' + line for line in code.splitlines()) else: lines.append(code) lines.append('} %s;' % feature.name) feature_defs.append('\n'.join(lines)) fea_str = '\n\n'.join(feature_defs) gdef_str = _build_gdef(ufo) # make sure feature text is a unicode string, for defcon full_text = '\n\n'.join( filter(None, [prefix_str, class_str, fea_str, gdef_str])) + '\n' ufo.features.text = full_text if full_text.strip() else '' def _build_gdef(ufo): """Build a table GDEF statement for ligature carets.""" from glyphsLib import glyphdata # Expensive import bases, ligatures, marks, carets = set(), set(), set(), {} category_key = GLYPHLIB_PREFIX + 'category' subCategory_key = GLYPHLIB_PREFIX + 'subCategory' for glyph in ufo: has_attaching_anchor = False for anchor in glyph.anchors: name = anchor.name if name and not name.startswith('_'): has_attaching_anchor = True if name and name.startswith('caret_') and 'x' in anchor: carets.setdefault(glyph.name, []).append(round(anchor['x'])) lib = glyph.lib glyphinfo = glyphdata.get_glyph(glyph.name) # first check glyph.lib for category/subCategory overrides; else use # global values from GlyphData category = lib.get(category_key) if category is None: category = glyphinfo.category subCategory = lib.get(subCategory_key) if subCategory is None: subCategory = glyphinfo.subCategory # Glyphs.app assigns glyph classes like this: # # * Base: any glyph that has an attaching anchor # (such as "top"; "_top" does not count) and is neither # classified as Ligature nor Mark using the definitions below; # # * Ligature: if subCategory is "Ligature" and the glyph has # at least one attaching anchor; # # * Mark: if category is "Mark" and subCategory is either # "Nonspacing" or "Spacing Combining"; # # * Compound: never assigned by Glyphs.app. # # https://github.com/googlei18n/glyphsLib/issues/85 # https://github.com/googlei18n/glyphsLib/pull/100#issuecomment-275430289 if subCategory == 'Ligature' and has_attaching_anchor: ligatures.add(glyph.name) elif category == 'Mark' and (subCategory == 'Nonspacing' or subCategory == 'Spacing Combining'): marks.add(glyph.name) elif has_attaching_anchor: bases.add(glyph.name) if not any((bases, ligatures, marks, carets)): return None lines = ['table GDEF {', ' # automatic'] glyphOrder = ufo.lib[PUBLIC_PREFIX + 'glyphOrder'] glyphIndex = lambda glyph: glyphOrder.index(glyph) fmt = lambda g: ('[%s]' % ' '.join(sorted(g, key=glyphIndex))) if g else '' lines.extend([ ' GlyphClassDef', ' %s, # Base' % fmt(bases), ' %s, # Liga' % fmt(ligatures), ' %s, # Mark' % fmt(marks), ' ;']) for glyph, caretPos in sorted(carets.items()): lines.append(' LigatureCaretByPos %s %s;' % (glyph, ' '.join(unicode(p) for p in sorted(caretPos)))) lines.append('} GDEF;') return '\n'.join(lines) def replace_feature(tag, repl, features): if not repl.endswith("\n"): repl += "\n" return re.sub( r"(?<=^feature %(tag)s {\n)(.*?)(?=^} %(tag)s;$)" % {"tag": tag}, repl, features, count=1, flags=re.DOTALL | re.MULTILINE) glyphslib-2.2.1/Lib/glyphsLib/builder/filters.py000066400000000000000000000046271322341616200216350ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) import logging import re from glyphsLib.util import cast_to_number_or_bool logger = logging.getLogger(__name__) def parse_glyphs_filter(filter_str, is_pre=False): """Parses glyphs custom filter string into a dict object that ufo2ft can consume. Reference: ufo2ft: https://github.com/googlei18n/ufo2ft Glyphs 2.3 Handbook July 2016, p184 Args: filter_str - a string of glyphs app filter Return: A dictionary contains the structured filter. Return None if parse failed. """ elements = filter_str.split(';') if elements[0] == '': logger.error('Failed to parse glyphs filter, expecting a filter name: \ %s', filter_str) return None result = {} result['name'] = elements[0] for idx, elem in enumerate(elements[1:]): if not elem: # skip empty arguments continue if ':' in elem: # Key value pair key, value = elem.split(':', 1) if key.lower() in ['include', 'exclude']: if idx != len(elements[1:]) - 1: logger.error('{} can only present as the last argument in the filter. {} is ignored.'.format(key, elem)) continue result[key.lower()] = re.split('[ ,]+', value) else: if 'kwargs' not in result: result['kwargs'] = {} result['kwargs'][key] = cast_to_number_or_bool(value) else: if 'args' not in result: result['args'] = [] result['args'].append(cast_to_number_or_bool(elem)) if is_pre: result['pre'] = True return result glyphslib-2.2.1/Lib/glyphsLib/builder/font.py000066400000000000000000000105071322341616200211250ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from collections import deque, OrderedDict import logging from .common import to_ufo_time from .constants import GLYPHS_PREFIX logger = logging.getLogger(__name__) def to_ufo_font_attributes(self, family_name): """Generate a list of UFOs with metadata loaded from .glyphs data. Modifies the list of UFOs in the UFOBuilder (self) in-place. """ font = self.font # "date" can be missing; Glyphs.app removes it on saving if it's empty: # https://github.com/googlei18n/glyphsLib/issues/134 date_created = getattr(font, 'date', None) if date_created is not None: date_created = to_ufo_time(date_created) units_per_em = font.upm version_major = font.versionMajor version_minor = font.versionMinor copyright = font.copyright designer = font.designer designer_url = font.designerURL manufacturer = font.manufacturer manufacturer_url = font.manufacturerURL for master in font.masters: ufo = self.ufo_module.Font() if date_created is not None: ufo.info.openTypeHeadCreated = date_created ufo.info.unitsPerEm = units_per_em ufo.info.versionMajor = version_major ufo.info.versionMinor = version_minor if copyright: ufo.info.copyright = copyright if designer: ufo.info.openTypeNameDesigner = designer if designer_url: ufo.info.openTypeNameDesignerURL = designer_url if manufacturer: ufo.info.openTypeNameManufacturer = manufacturer if manufacturer_url: ufo.info.openTypeNameManufacturerURL = manufacturer_url ufo.info.ascender = master.ascender ufo.info.capHeight = master.capHeight ufo.info.descender = master.descender ufo.info.xHeight = master.xHeight horizontal_stems = master.horizontalStems vertical_stems = master.verticalStems italic_angle = -master.italicAngle if horizontal_stems: ufo.info.postscriptStemSnapH = horizontal_stems if vertical_stems: ufo.info.postscriptStemSnapV = vertical_stems if italic_angle: ufo.info.italicAngle = italic_angle width = master.width weight = master.weight if weight: ufo.lib[GLYPHS_PREFIX + 'weight'] = weight if width: ufo.lib[GLYPHS_PREFIX + 'width'] = width for number in ('', '1', '2', '3'): custom_name = getattr(master, 'customName' + number) if custom_name: ufo.lib[GLYPHS_PREFIX + 'customName' + number] = custom_name custom_value = getattr(master, 'customValue' + number) if custom_value: ufo.lib[GLYPHS_PREFIX + 'customValue' + number] = custom_value self.to_ufo_names(ufo, master, family_name) self.to_ufo_blue_values(ufo, master) self.to_ufo_family_user_data(ufo) self.to_ufo_master_user_data(ufo, master) self.to_ufo_guidelines(ufo, master) self.to_ufo_custom_params(ufo, master) master_id = master.id ufo.lib[GLYPHS_PREFIX + 'fontMasterID'] = master_id # FIXME: (jany) in the future, yield this UFO (for memory, laze iter) self._ufos[master_id] = ufo def to_glyphs_font_attributes(self, ufo, master, is_initial): """ Copy font attributes from `ufo` either to `self.font` or to `master`. Arguments: self -- The UFOBuilder ufo -- The current UFO being read master -- The current master being written is_initial -- True iff this the first UFO that we process """ master.id = ufo.lib[GLYPHS_PREFIX + 'fontMasterID'] # TODO: all the other attributes glyphslib-2.2.1/Lib/glyphsLib/builder/glyph.py000066400000000000000000000156151322341616200213070ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) import logging logger = logging.getLogger(__name__) import glyphsLib from .common import to_ufo_time from .constants import (GLYPHLIB_PREFIX, GLYPHS_COLORS, GLYPHS_PREFIX, PUBLIC_PREFIX) def to_ufo_glyph(self, ufo_glyph, layer, glyph_data): """Add .glyphs metadata, paths, components, and anchors to a glyph.""" from glyphsLib import glyphdata # Expensive import uval = glyph_data.unicode if uval is not None: ufo_glyph.unicode = int(uval, 16) note = glyph_data.note if note is not None: ufo_glyph.note = note last_change = glyph_data.lastChange if last_change is not None: ufo_glyph.lib[GLYPHLIB_PREFIX + 'lastChange'] = to_ufo_time(last_change) color_index = glyph_data.color if color_index is not None: ufo_glyph.lib[GLYPHLIB_PREFIX + 'ColorIndex'] = color_index color_tuple = None if isinstance(color_index, list): if not all(i in range(0, 256) for i in color_index): logger.warn('Invalid color tuple {} for glyph {}. ' 'Values must be in range 0-255'.format(color_index, glyph_data.name)) else: color_tuple = ','.join('{0:.4f}'.format(i/255) if i in range(1, 255) else str(i//255) for i in color_index) elif isinstance(color_index, int) and color_index in range(len(GLYPHS_COLORS)): color_tuple = GLYPHS_COLORS[color_index] else: logger.warn('Invalid color index {} for {}'.format(color_index, glyph_data.name)) if color_tuple is not None: ufo_glyph.lib[PUBLIC_PREFIX + 'markColor'] = color_tuple export = glyph_data.export if not export: ufo_glyph.lib[GLYPHLIB_PREFIX + 'Export'] = export glyphinfo = glyphdata.get_glyph(ufo_glyph.name) production_name = glyph_data.production or glyphinfo.production_name if production_name != ufo_glyph.name: postscriptNamesKey = PUBLIC_PREFIX + 'postscriptNames' if postscriptNamesKey not in ufo_glyph.font.lib: ufo_glyph.font.lib[postscriptNamesKey] = dict() ufo_glyph.font.lib[postscriptNamesKey][ufo_glyph.name] = production_name for key in ['leftMetricsKey', 'rightMetricsKey', 'widthMetricsKey']: glyph_metrics_key = getattr(layer, key) if glyph_metrics_key is None: glyph_metrics_key = getattr(glyph_data, key) if glyph_metrics_key: ufo_glyph.lib[GLYPHLIB_PREFIX + key] = glyph_metrics_key # if glyph contains custom 'category' and 'subCategory' overrides, store # them in the UFO glyph's lib category = glyph_data.category if category is None: category = glyphinfo.category else: ufo_glyph.lib[GLYPHLIB_PREFIX + 'category'] = category subCategory = glyph_data.subCategory if subCategory is None: subCategory = glyphinfo.subCategory else: ufo_glyph.lib[GLYPHLIB_PREFIX + 'subCategory'] = subCategory # load width before background, which is loaded with lib data width = layer.width if width is None: pass elif category == 'Mark' and subCategory == 'Nonspacing' and width > 0: # zero the width of Nonspacing Marks like Glyphs.app does on export # TODO: check for customParameter DisableAllAutomaticBehaviour ufo_glyph.lib[GLYPHLIB_PREFIX + 'originalWidth'] = width ufo_glyph.width = 0 else: ufo_glyph.width = width self.to_ufo_glyph_libdata(ufo_glyph, layer) pen = ufo_glyph.getPointPen() self.to_ufo_draw_paths(pen, layer.paths) self.to_ufo_draw_components(pen, layer.components) self.to_ufo_glyph_anchors(ufo_glyph, layer.anchors) def to_ufo_glyph_background(self, glyph, background): """Set glyph background.""" if not background: return if glyph.layer.name != 'public.default': layer_name = glyph.layer.name + '.background' else: layer_name = 'public.background' font = glyph.font if layer_name not in font.layers: layer = font.newLayer(layer_name) else: layer = font.layers[layer_name] new_glyph = layer.newGlyph(glyph.name) new_glyph.width = glyph.width pen = new_glyph.getPointPen() self.to_ufo_draw_paths(pen, background.paths) self.to_ufo_draw_components(pen, background.components) self.to_ufo_glyph_anchors(new_glyph, background.anchors) self.to_ufo_guidelines(new_glyph, background) def to_ufo_glyph_libdata(self, glyph, layer): """Add to a glyph's lib data.""" self.to_ufo_guidelines(glyph, layer) self.to_ufo_glyph_background(glyph, layer.background) for key in ['annotations', 'hints']: try: value = getattr(layer, key) except KeyError: continue if key == 'annotations': annotations = [] for an in list(value.values()): annot = {} for attr in ['angle', 'position', 'text', 'type', 'width']: val = getattr(an, attr, None) if attr == 'position' and val: val = list(val) if val: annot[attr] = val annotations.append(annot) value = annotations elif key == 'hints': hints = [] for hi in value: hint = {} for attr in ['horizontal', 'options', 'stem', 'type']: val = getattr(hi, attr, None) hint[attr] = val for attr in ['origin', 'other1', 'other2', 'place', 'scale', 'target']: val = getattr(hi, attr, None) if val is not None and not any(v is None for v in val): hint[attr] = list(val) hints.append(hint) value = hints if value: glyph.lib[GLYPHS_PREFIX + key] = value # data related to components stored in lists of booleans # each list's elements correspond to the components in order for key in ['alignment', 'locked']: values = [getattr(c, key) for c in layer.components] if any(values): key = key[0].upper() + key[1:] glyph.lib['%scomponents%s' % (GLYPHS_PREFIX, key)] = values glyphslib-2.2.1/Lib/glyphsLib/builder/guidelines.py000066400000000000000000000023131322341616200223030ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) def to_ufo_guidelines(self, ufo_obj, glyphs_obj): """Set guidelines.""" guidelines = glyphs_obj.guides if not guidelines: return new_guidelines = [] for guideline in guidelines: x, y = guideline.position angle = guideline.angle new_guideline = {'x': x, 'y': y, 'angle': (360 - angle) % 360} new_guidelines.append(new_guideline) ufo_obj.guidelines = new_guidelines def to_glyphs_guidelines(self, glyphs_obj, ufo_obj): """Set guidelines.""" pass glyphslib-2.2.1/Lib/glyphsLib/builder/kerning.py000066400000000000000000000077421322341616200216230ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) import logging import re logger = logging.getLogger(__name__) def to_ufo_kerning(self, ufo, kerning_data): """Add .glyphs kerning to an UFO.""" warning_msg = 'Non-existent glyph class %s found in kerning rules.' class_glyph_pairs = [] for left, pairs in kerning_data.items(): match = re.match(r'@MMK_L_(.+)', left) left_is_class = bool(match) if left_is_class: left = 'public.kern1.%s' % match.group(1) if left not in ufo.groups: logger.warn(warning_msg % left) continue for right, kerning_val in pairs.items(): match = re.match(r'@MMK_R_(.+)', right) right_is_class = bool(match) if right_is_class: right = 'public.kern2.%s' % match.group(1) if right not in ufo.groups: logger.warn(warning_msg % right) continue if left_is_class != right_is_class: if left_is_class: pair = (left, right, True) else: pair = (right, left, False) class_glyph_pairs.append(pair) ufo.kerning[left, right] = kerning_val seen = {} for classname, glyph, is_left_class in reversed(class_glyph_pairs): _remove_rule_if_conflict(ufo, seen, classname, glyph, is_left_class) def _remove_rule_if_conflict(ufo, seen, classname, glyph, is_left_class): """Check if a class-to-glyph kerning rule has a conflict with any existing rule in `seen`, and remove any conflicts if they exist. """ original_pair = (classname, glyph) if is_left_class else (glyph, classname) val = ufo.kerning[original_pair] rule = original_pair + (val,) old_glyphs = ufo.groups[classname] new_glyphs = [] for member in old_glyphs: pair = (member, glyph) if is_left_class else (glyph, member) existing_rule = seen.get(pair) if (existing_rule is not None and existing_rule[-1] != val and pair not in ufo.kerning): logger.warn( 'Conflicting kerning rules found in %s master for glyph pair ' '"%s, %s" (%s and %s), removing pair from latter rule' % ((ufo.info.styleName,) + pair + (existing_rule, rule))) else: new_glyphs.append(member) seen[pair] = rule if new_glyphs != old_glyphs: del ufo.kerning[original_pair] for member in new_glyphs: pair = (member, glyph) if is_left_class else (glyph, member) ufo.kerning[pair] = val def to_ufo_glyph_groups(self, kerning_groups, glyph_data): """Add a glyph to its kerning groups, creating new groups if necessary.""" glyph_name = glyph_data.name group_keys = { '1': 'rightKerningGroup', '2': 'leftKerningGroup'} for side, group_key in group_keys.items(): group = getattr(glyph_data, group_key) if group is None or len(group) == 0: continue group = 'public.kern%s.%s' % (side, group) kerning_groups[group] = kerning_groups.get(group, []) + [glyph_name] def to_ufo_kerning_groups(self, ufo, kerning_groups): """Add kerning groups to an UFO.""" for name, glyphs in kerning_groups.items(): ufo.groups[name] = glyphs glyphslib-2.2.1/Lib/glyphsLib/builder/names.py000066400000000000000000000070331322341616200212620ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from collections import deque def to_ufo_names(self, ufo, master, family_name): width = master.width weight = master.weight custom = master.customName is_italic = bool(master.italicAngle) styleName = build_style_name( width if width != 'Regular' else '', weight if weight != 'Regular' else '', custom, is_italic ) styleMapFamilyName, styleMapStyleName = build_stylemap_names( family_name=family_name, style_name=styleName, is_bold=(styleName == 'Bold'), is_italic=is_italic ) ufo.info.familyName = family_name ufo.info.styleName = styleName ufo.info.styleMapFamilyName = styleMapFamilyName ufo.info.styleMapStyleName = styleMapStyleName def build_stylemap_names(family_name, style_name, is_bold=False, is_italic=False, linked_style=None): """Build UFO `styleMapFamilyName` and `styleMapStyleName` based on the family and style names, and the entries in the "Style Linking" section of the "Instances" tab in the "Font Info". The value of `styleMapStyleName` can be either "regular", "bold", "italic" or "bold italic", depending on the values of `is_bold` and `is_italic`. The `styleMapFamilyName` is a combination of the `family_name` and the `linked_style`. If `linked_style` is unset or set to 'Regular', the linked style is equal to the style_name with the last occurrences of the strings 'Regular', 'Bold' and 'Italic' stripped from it. """ styleMapStyleName = ' '.join(s for s in ( 'bold' if is_bold else '', 'italic' if is_italic else '') if s) or 'regular' if not linked_style or linked_style == 'Regular': linked_style = _get_linked_style(style_name, is_bold, is_italic) if linked_style: styleMapFamilyName = family_name + ' ' + linked_style else: styleMapFamilyName = family_name return styleMapFamilyName, styleMapStyleName def build_style_name(width='', weight='', custom='', is_italic=False): """Build style name from width, weight, and custom style strings and whether the style is italic. """ return ' '.join( s for s in (custom, width, weight, 'Italic' if is_italic else '') if s ) or 'Regular' def _get_linked_style(style_name, is_bold, is_italic): # strip last occurrence of 'Regular', 'Bold', 'Italic' from style_name # depending on the values of is_bold and is_italic linked_style = deque() is_regular = not (is_bold or is_italic) for part in reversed(style_name.split()): if part == 'Regular' and is_regular: is_regular = False elif part == 'Bold' and is_bold: is_bold = False elif part == 'Italic' and is_italic: is_italic = False else: linked_style.appendleft(part) return ' '.join(linked_style) glyphslib-2.2.1/Lib/glyphsLib/builder/paths.py000066400000000000000000000032211322341616200212710ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) def to_ufo_draw_paths(self, pen, paths): """Draw .glyphs paths onto a pen.""" for path in paths: pen.beginPath() nodes = list(path.nodes) # the list is changed below, otherwise you can't draw more than once per session. if not nodes: pen.endPath() continue if not path.closed: node = nodes.pop(0) assert node.type == 'line', 'Open path starts with off-curve points' pen.addPoint(tuple(node.position), segmentType='move') else: # In Glyphs.app, the starting node of a closed contour is always # stored at the end of the nodes list. nodes.insert(0, nodes.pop()) for node in nodes: node_type = node.type if node_type not in ['line', 'curve', 'qcurve']: node_type = None pen.addPoint(tuple(node.position), segmentType=node_type, smooth=node.smooth) pen.endPath() glyphslib-2.2.1/Lib/glyphsLib/builder/user_data.py000066400000000000000000000030371322341616200221260ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from .constants import GLYPHS_PREFIX MASTER_USER_DATA_KEY = GLYPHS_PREFIX + 'fontMaster.userData' def to_ufo_family_user_data(self, ufo): """Set family-wide user data as Glyphs does.""" user_data = self.font.userData for key in user_data.keys(): ufo.lib[key] = user_data[key] def to_ufo_master_user_data(self, ufo, master): """Set master-specific user data as Glyphs does.""" user_data = master.userData if user_data: data = {} for key in user_data.keys(): data[key] = user_data[key] ufo.lib[MASTER_USER_DATA_KEY] = data def to_glyphs_family_user_data(self, ufo): """Set the GSFont userData from the UFO family-wide user data.""" pass def to_glyphs_master_user_data(self, ufo, master): """Set the GSFontMaster userData from the UFO master-specific user data.""" pass glyphslib-2.2.1/Lib/glyphsLib/classes.py000077500000000000000000002517571322341616200202070ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright 2016 Georg Seifert. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function, division, unicode_literals import re import math import inspect import traceback import uuid import logging import glyphsLib from glyphsLib.types import ( transform, point, rect, size, glyphs_datetime, color, floatToString, readIntlist, writeIntlist, baseType) from glyphsLib.parser import Parser from glyphsLib.writer import Writer, escape_string from collections import OrderedDict from fontTools.misc.py23 import unicode, basestring, UnicodeIO, unichr, open from glyphsLib.affine import Affine logger = logging.getLogger(__name__) __all__ = [ "Glyphs", "GSFont", "GSFontMaster", "GSAlignmentZone", "GSInstance", "GSCustomParameter", "GSClass", "GSFeaturePrefix", "GSFeature", "GSGlyph", "GSLayer", "GSAnchor", "GSComponent", "GSSmartComponentAxis", "GSPath", "GSNode", "GSGuideLine", "GSAnnotation", "GSHint", "GSBackgroundImage", # Constants "MOVE", "LINE", "CURVE", "OFFCURVE", "GSMOVE", "GSLINE", "GSCURVE", "GSOFFCURVE", "GSSHARP", "GSSMOOTH", "TAG", "TOPGHOST", "STEM", "BOTTOMGHOST", "TTANCHOR", "TTSTEM", "TTALIGN", "TTINTERPOLATE", "TTDIAGONAL", "TTDELTA", "CORNER", "CAP", "TTDONTROUND", "TTROUND", "TTROUNDUP", "TTROUNDDOWN", "TRIPLE", "TEXT", "ARROW", "CIRCLE", "PLUS", "MINUS", "LTR", "RTL", "LTRTTB", "RTLTTB", "GSTopLeft", "GSTopCenter", "GSTopRight", "GSCenterLeft", "GSCenterCenter", "GSCenterRight", "GSBottomLeft", "GSBottomCenter", "GSBottomRight", ] # CONSTANTS GSMOVE_ = 17 GSLINE_ = 1 GSCURVE_ = 35 GSOFFCURVE_ = 65 GSSHARP = 0 GSSMOOTH = 100 GSMOVE = "move" GSLINE = "line" GSCURVE = "curve" GSQCURVE = "qcurve" GSOFFCURVE = "offcurve" MOVE = "move" LINE = "line" CURVE = "curve" OFFCURVE = "offcurve" TAG = -2 TOPGHOST = -1 STEM = 0 BOTTOMGHOST = 1 TTANCHOR = 2 TTSTEM = 3 TTALIGN = 4 TTINTERPOLATE = 5 TTDIAGONAL = 6 TTDELTA = 7 CORNER = 16 CAP = 17 TTDONTROUND = 4 TTROUND = 0 TTROUNDUP = 1 TTROUNDDOWN = 2 TRIPLE = 128 # Annotations: TEXT = 1 ARROW = 2 CIRCLE = 3 PLUS = 4 MINUS = 5 # Reverse lookup for __repr__ hintConstants = { -2: 'Tag', -1: 'TopGhost', 0: 'Stem', 1: 'BottomGhost', 2: 'TTAnchor', 3: 'TTStem', 4: 'TTAlign', 5: 'TTInterpolate', 6: 'TTDiagonal', 7: 'TTDelta', 16: 'Corner', 17: 'Cap', } GSTopLeft = 6 GSTopCenter = 7 GSTopRight = 8 GSCenterLeft = 3 GSCenterCenter = 4 GSCenterRight = 5 GSBottomLeft = 0 GSBottomCenter = 1 GSBottomRight = 2 # Writing direction LTR = 0 RTL = 1 LTRTTB = 3 RTLTTB = 2 class OnlyInGlyphsAppError(NotImplementedError): def __init__(self): NotImplementedError.__init__(self, "This property/method is only available in the real UI-based version of Glyphs.app.") def hint_target(line=None): if line is None: return None if line[0] == "{": return point(line) else: return line def isString(string): return isinstance(string, (str, unicode)) def transformStructToScaleAndRotation(transform): Det = transform[0] * transform[3] - transform[1] * transform[2] _sX = math.sqrt(math.pow(transform[0], 2) + math.pow(transform[1], 2)) _sY = math.sqrt(math.pow(transform[2], 2) + math.pow(transform[3], 2)) if Det < 0: _sY = -_sY _R = math.atan2(transform[1] * _sY, transform[0] * _sX) * 180 / math.pi if Det < 0 and (math.fabs(_R) > 135 or _R < -90): _sX = -_sX _sY = -_sY if _R < 0: _R += 180 else: _R -= 180 quadrant = 0 if _R < -90: quadrant = 180 _R += quadrant if _R > 90: quadrant = -180 _R += quadrant _R = _R * _sX / _sY _R -= quadrant if _R < -179: _R += 360 return _sX, _sY, _R class GSApplication(object): def __init__(self): self.font = None self.fonts = [] def open(self, path): newFont = GSFont(path) self.fonts.append(newFont) self.font = newFont return newFont def __repr__(self): return '' Glyphs = GSApplication() class GSBase(object): _classesForName = {} _defaultsForName = {} _wrapperKeysTranslate = {} def __init__(self): for key in self._classesForName.keys(): if not hasattr(self, key): klass = self._classesForName[key] if inspect.isclass(klass) and issubclass(klass, GSBase): value = [] elif key in self._defaultsForName: value = self._defaultsForName.get(key) else: value = klass() key = self._wrapperKeysTranslate.get(key, key) setattr(self, key, value) def __repr__(self): content = "" if hasattr(self, "_dict"): content = str(self._dict) return "<%s %s>" % (self.__class__.__name__, content) def classForName(self, name): return self._classesForName.get(name, str) # Note: # The dictionary API exposed by GS* classes is "private" in the sense that: # * it should only be used by the parser, so it should only # work for key names that are found in the files # * and only for filling data in the objects, which is why it only # implements `__setitem__` # # Users of the library should only rely on the object-oriented API that is # documented at https://docu.glyphsapp.com/ def __setitem__(self, key, value): if isinstance(value, bytes) and key in self._classesForName: new_type = self._classesForName[key] if new_type is unicode: value = value.decode('utf-8') else: try: value = new_type().read(value) except: value = new_type(value) key = self._wrapperKeysTranslate.get(key, key) setattr(self, key, value) def shouldWriteValueForKey(self, key): getKey = self._wrapperKeysTranslate.get(key, key) value = getattr(self, getKey) klass = self._classesForName[key] default = self._defaultsForName.get(key, None) if (isinstance(value, (list, glyphsLib.classes.Proxy, str, unicode)) and len(value) == 0): return False if default is not None: return default != value if klass in (int, float, bool) and value == 0: return False if isinstance(value, baseType) and value.value is None: return False return True class Proxy(object): def __init__(self, owner): self._owner = owner def __repr__(self): """Return list-lookalike of representation string of objects""" strings = [] for currItem in self: strings.append("%s" % (currItem)) return "(%s)" % (', '.join(strings)) def __len__(self): values = self.values() if values is not None: return len(values) return 0 def pop(self, i): if type(i) == int: node = self[i] del self[i] return node else: raise(KeyError) def __iter__(self): values = self.values() if values is not None: for element in values: yield element def index(self, value): return self.values().index(value) def __copy__(self): return list(self) def __deepcopy__(self, memo): return [x.copy() for x in self.values()] def setter(self, values): method = self.setterMethod() if type(values) == list: method(values) elif (type(values) == tuple or values.__class__.__name__ == "__NSArrayM" or type(values) == type(self)): method(list(values)) elif values is None: method(list()) else: raise TypeError class LayersIterator: def __init__(self, owner): self.curInd = 0 self._owner = owner self._orderedLayers = None def __iter__(self): return self def next(self): return self.__next__() def __next__(self): if self._owner.parent: if self.curInd >= len(self._owner.layers): raise StopIteration item = self.orderedLayers[self.curInd] else: if self.curInd >= len(self._owner._layers): raise StopIteration item = self._owner._layers[self.curInd] self.curInd += 1 return item @property def orderedLayers(self): if not self._orderedLayers: glyphLayerIds = [ l.associatedMasterId for l in self._owner._layers.values() ] masterIds = [m.id for m in self._owner.parent.masters] intersectedLayerIds = set(glyphLayerIds) & set(masterIds) orderedLayers = [ self._owner._layers.get(m.id) for m in self._owner.parent.masters if m.id in intersectedLayerIds ] orderedLayers += [ self._owner._layers.get(l.layerId) for l in self._owner._layers.values() if l.layerId not in intersectedLayerIds ] self._orderedLayers = orderedLayers return self._orderedLayers class FontFontMasterProxy(Proxy): """The list of masters. You can access it with the index or the master ID. Usage: Font.masters[index] Font.masters[id] for master in Font.masters: ... """ def __getitem__(self, Key): if type(Key) == slice: return self.values().__getitem__(Key) if type(Key) is int: if Key < 0: Key = self.__len__() + Key return self.values()[Key] elif isString(Key): for master in self.values(): if master.id == Key: return master else: raise(KeyError) def __setitem__(self, Key, FontMaster): FontMaster.font = self._owner if type(Key) is int: OldFontMaster = self.__getitem__(Key) if Key < 0: Key = self.__len__() + Key FontMaster.id = OldFontMaster.id self._owner._masters[Key] = FontMaster elif isString(Key): OldFontMaster = self.__getitem__(Key) FontMaster.id = OldFontMaster.id Index = self._owner._masters.index(OldFontMaster) self._owner._masters[Index] = FontMaster else: raise(KeyError) def __delitem__(self, Key): if type(Key) is int: if Key < 0: Key = self.__len__() + Key return self.remove(self._owner._masters[Key]) else: OldFontMaster = self.__getitem__(Key) return self.remove(OldFontMaster) def values(self): return self._owner._masters def append(self, FontMaster): FontMaster.font = self._owner FontMaster.id = str(uuid.uuid4()).upper() self._owner._masters.append(FontMaster) # Cycle through all glyphs and append layer for glyph in self._owner.glyphs: if not glyph.layers[FontMaster.id]: newLayer = GSLayer() glyph._setupLayer(newLayer, FontMaster.id) glyph.layers.append(newLayer) def remove(self, FontMaster): # First remove all layers in all glyphs that reference this master for glyph in self._owner.glyphs: for layer in glyph.layers: if layer.associatedMasterId == FontMaster.id or layer.layerId == FontMaster.id: glyph.layers.remove(layer) self._owner._masters.remove(FontMaster) def insert(self, Index, FontMaster): FontMaster.font = self._owner self._owner._masters.insert(Index, FontMaster) def extend(self, FontMasters): for FontMaster in FontMasters: self.append(FontMaster) def setter(self, values): if isinstance(values, Proxy): values = list(values) self._owner._masters = values for m in self._owner._masters: m.font = self._owner class FontGlyphsProxy(Proxy): """The list of glyphs. You can access it with the index or the glyph name. Usage: Font.glyphs[index] Font.glyphs[name] for glyph in Font.glyphs: ... """ def __getitem__(self, key): if type(key) == slice: return self.values().__getitem__(key) # by index if isinstance(key, int): return self._owner._glyphs[key] if isinstance(key, basestring): # by glyph name for glyph in self._owner._glyphs: if glyph.name == key: return glyph # by string representation as u'ä' if len(key) == 1: for glyph in self._owner._glyphs: if glyph.unicode == "%04X" % (ord(key)): return glyph # by unicode else: for glyph in self._owner._glyphs: if glyph.unicode == key.upper(): return glyph return None def __setitem__(self, key, glyph): if type(key) is int: self._owner._setupGlyph(glyph) self._owner._glyphs[key] = glyph else: raise KeyError # TODO: add other access methods def __delitem__(self, key): if type(key) is int: del(self._owner._glyph[key]) else: raise KeyError # TODO: add other access methods def __contains__(self, item): if isString(item): raise "not implemented" return item in self._owner._glyphs def values(self): return self._owner._glyphs def items(self): items = [] for value in self._owner._glyphs: key = value.name items.append((key, value)) return items def append(self, glyph): self._owner._setupGlyph(glyph) self._owner._glyphs.append(glyph) def extend(self, objects): for glyph in objects: self._owner._setupGlyph(glyph) self._owner._glyphs.extend(list(objects)) def __len__(self): return len(self._owner._glyphs) def setter(self, values): if isinstance(values, Proxy): values = list(values) self._owner._glyphs = values for g in self._owner._glyphs: g.parent = self._owner for layer in g.layers.values(): if (not hasattr(layer, "associatedMasterId") or layer.associatedMasterId is None or len(layer.associatedMasterId) == 0): g._setupLayer(layer, layer.layerId) class FontClassesProxy(Proxy): def __getitem__(self, key): if isinstance(key, (slice, int)): return self.values().__getitem__(key) if isinstance(key, (str, unicode)): for index, klass in enumerate(self.values()): if klass.name == key: return self.values()[index] raise KeyError def __setitem__(self, key, value): if isinstance(key, int): self.values()[key] = value value._parent = self._owner elif isinstance(key, (str, unicode)): for index, klass in enumerate(self.values()): if klass.name == key: self.values()[index] = value value._parent = self._owner else: raise KeyError def __delitem__(self, key): if isinstance(key, int): del self.values()[key] elif isinstance(key, (str, unicode)): for index, klass in enumerate(self.values()): if klass.name == key: del self.values()[index] def append(self, item): self.values().append(item) item._parent = self._owner def insert(self, key, item): self.values().insert(key, item) item._parent = self._owner def extend(self, items): self.values().extend(items) for value in items: value._parent = self._owner def remove(self, item): self.values().remove(item) def values(self): return self._owner._classes def setter(self, values): if isinstance(values, Proxy): values = list(values) self._owner._classes = values for value in values: value._parent = self._owner class GlyphLayerProxy(Proxy): def __getitem__(self, key): self._ensureMasterLayers() if isinstance(key, slice): return self.values().__getitem__(key) elif isinstance(key, int): if self._owner.parent: return list(self)[key] return list(self.values())[key] elif isString(key): if key in self._owner._layers: return self._owner._layers[key] def __setitem__(self, key, layer): if isinstance(key, int) and self._owner.parent: OldLayer = self._owner._layers[key] if key < 0: key = self.__len__() + key layer.layerId = OldLayer.layerId layer.associatedMasterId = OldLayer.associatedMasterId self._owner._setupLayer(layer, OldLayer.layerId) self._owner._layers[key] = layer # TODO: replace by ID else: raise KeyError def __delitem__(self, key): if isinstance(key, int) and self._owner.parent: if key < 0: key = self.__len__() + key Layer = self.__getitem__(key) key = Layer.layerId del(self._owner._layers[key]) def __iter__(self): return LayersIterator(self._owner) def __len__(self): return len(self.values()) def keys(self): self._ensureMasterLayers() return self._owner._layers.keys() def values(self): self._ensureMasterLayers() return self._owner._layers.values() def append(self, layer): assert layer is not None self._ensureMasterLayers() if not layer.associatedMasterId: layer.associatedMasterId = self._owner.parent.masters[0].id if not layer.layerId: layer.layerId = str(uuid.uuid4()).upper() self._owner._setupLayer(layer, layer.layerId) self._owner._layers[layer.layerId] = layer def extend(self, layers): for layer in layers: self.append(layer) def remove(self, layer): return self._owner.removeLayerForKey_(layer.layerId) def insert(self, index, layer): self._ensureMasterLayers() self.append(layer) def setter(self, values): newLayers = OrderedDict() if (type(values) == list or type(values) == tuple or type(values) == type(self)): for layer in values: newLayers[layer.layerId] = layer elif type(values) == dict: # or isinstance(values, NSDictionary) for (key, layer) in values.items(): newLayers[layer.layerId] = layer else: raise TypeError for (key, layer) in newLayers.items(): self._owner._setupLayer(layer, key) self._owner._layers = newLayers def _ensureMasterLayers(self): # Ensure existence of master-linked layers (even for iteration, len() etc.) if accidentally deleted if not self._owner.parent: return for master in self._owner.parent.masters: if self._owner.parent.masters[master.id] is None: newLayer = GSLayer() newLayer.associatedMasterId = master.id newLayer.layerId = master.id self._owner._setupLayer(newLayer, master.id) self.__setitem__(master.id, newLayer) def plistArray(self): return list(self._owner._layers.values()) class LayerAnchorsProxy(Proxy): def __getitem__(self, key): if isinstance(key, (slice, int)): return self.values().__getitem__(key) elif isinstance(key, (str, unicode)): for i, a in enumerate(self._owner._anchors): if a.name == key: return self._owner._anchors[i] else: raise KeyError def __setitem__(self, key, anchor): if isinstance(key, (str, unicode)): anchor.name = key for i, a in enumerate(self._owner._anchors): if a.name == key: self._owner._anchors[i] = anchor return anchor._parent = self._owner self._owner._anchors.append(anchor) else: raise TypeError def __delitem__(self, key): if isinstance(key, int): del self._owner._anchors[key] elif isinstance(key, (str, unicode)): for i, a in enumerate(self._owner._anchors): if a.name == key: self._owner._anchors[i]._parent = None del self._owner._anchors[i] return def values(self): return self._owner._anchors def append(self, anchor): for i, a in enumerate(self._owner._anchors): if a.name == anchor.name: anchor._parent = self._owner self._owner._anchors[i] = anchor return if anchor.name: self._owner._anchors.append(anchor) else: raise ValueError("Anchor must have name") def extend(self, anchors): for anchor in anchors: anchor._parent = self._owner self._owner._anchors.extend(anchors) def remove(self, anchor): if isinstance(anchor, (str, unicode)): anchor = self.values()[anchor] return self._owner._anchors.remove(anchor) def insert(self, index, anchor): anchor._parent = self._owner self._owner._anchors.insert(index, anchor) def __len__(self): return len(self._owner._anchors) def setter(self, anchors): if isinstance(anchors, Proxy): anchors = list(anchors) self._owner._anchors = anchors for anchor in anchors: anchor._parent = self._owner class IndexedObjectsProxy(Proxy): def __getitem__(self, key): if isinstance(key, (slice, int)): return self.values().__getitem__(key) else: raise KeyError def __setitem__(self, key, value): if isinstance(key, int): self.values()[key] = value value._parent = self._owner else: raise KeyError def __delitem__(self, key): if isinstance(key, int): del self.values()[key] else: raise KeyError def values(self): return getattr(self._owner, self._objects_name) def append(self, value): self.values().append(value) value._parent = self._owner def extend(self, values): self.values().extend(values) for value in values: value._parent = self._owner def remove(self, value): self.values().remove(value) def insert(self, index, value): self.values().insert(index, value) value._parent = self._owner def __len__(self): return len(self.values()) def setter(self, values): setattr(self._owner, self._objects_name, list(values)) for value in self.values(): value._parent = self._owner class LayerPathsProxy(IndexedObjectsProxy): _objects_name = "_paths" def __init__(self, owner): super(LayerPathsProxy, self).__init__(owner) class LayerHintsProxy(IndexedObjectsProxy): _objects_name = "_hints" def __init__(self, owner): super(LayerHintsProxy, self).__init__(owner) class LayerComponentsProxy(IndexedObjectsProxy): _objects_name = "_components" def __init__(self, owner): super(LayerComponentsProxy, self).__init__(owner) class LayerAnnotationProxy(IndexedObjectsProxy): _objects_name = "_annotations" def __init__(self, owner): super(LayerAnnotationProxy, self).__init__(owner) class LayerGuideLinesProxy(IndexedObjectsProxy): _objects_name = "_guides" def __init__(self, owner): super(LayerGuideLinesProxy, self).__init__(owner) class PathNodesProxy(IndexedObjectsProxy): _objects_name = "_nodes" def __init__(self, owner): super(PathNodesProxy, self).__init__(owner) class CustomParametersProxy(Proxy): def __getitem__(self, key): if isinstance(key, slice): return self.values().__getitem__(key) if isinstance(key, int): return self._owner._customParameters[key] else: customParameter = self._get_parameter_by_key(key) if customParameter is not None: return customParameter.value return None def _get_parameter_by_key(self, key): for customParameter in self._owner._customParameters: if customParameter.name == key: return customParameter def __setitem__(self, key, value): customParameter = self._get_parameter_by_key(key) if customParameter is not None: customParameter.value = value else: parameter = GSCustomParameter(name=key, value=value) self._owner._customParameters.append(parameter) def __delitem__(self, key): if isinstance(key, int): del self._owner._customParameters[key] elif isinstance(key, basestring): for parameter in self._owner._customParameters: if parameter.name == key: self._owner._customParameters.remove(parameter) else: raise KeyError def __contains__(self, item): if isString(item): return self._owner.__getitem__(item) is not None return item in self._owner._customParameters def __iter__(self): for index in range(len(self._owner._customParameters)): yield self._owner._customParameters[index] def append(self, parameter): parameter.parent = self._owner self._owner._customParameters.append(parameter) def extend(self, parameters): for parameter in parameters: parameter.parent = self._owner self._owner._customParameters.extend(parameters) def remove(self, parameter): if isString(parameter): parameter = self.__getitem__(parameter) self._owner._customParameters.remove(parameter) def insert(self, index, parameter): parameter.parent = self._owner self._owner._customParameters.insert(index, parameter) def __len__(self): return len(self._owner._customParameters) def values(self): return self._owner._customParameters def __setter__(self, parameters): for parameter in parameters: parameter.parent = self._owner self._owner._customParameters = parameters def setterMethod(self): return self.__setter__ class UserDataProxy(Proxy): def __getitem__(self, key): if self._owner._userData is None: raise KeyError return self._owner._userData.get(key) def __setitem__(self, key, value): if self._owner._userData is not None: self._owner._userData[key] = value else: self._owner._userData = {key: value} def __delitem__(self, key): if self._owner._userData is not None and key in self._owner._userData: del self._owner._userData[key] def __contains__(self, item): if self._owner._userData is None: return False return item in self._owner._userData def __iter__(self): if self._owner._userData is None: return for value in self._owner._userData.values(): yield value def values(self): if self._owner._userData is None: return [] return self._owner._userData.values() def keys(self): if self._owner._userData is None: return [] return self._owner._userData.keys() def get(self, key): if self._owner._userData is None: return None return self._owner._userData.get(key) def setter(self, values): self._owner._userData = values class GSCustomParameter(GSBase): _classesForName = { "name": unicode, "value": None, } _CUSTOM_INT_PARAMS = frozenset(( 'ascender', 'blueShift', 'capHeight', 'descender', 'hheaAscender', 'hheaDescender', 'hheaLineGap', 'macintoshFONDFamilyID', 'openTypeHeadLowestRecPPEM', 'openTypeHheaAscender', 'openTypeHheaCaretOffset', 'openTypeHheaCaretSlopeRise', 'openTypeHheaCaretSlopeRun', 'openTypeHheaDescender', 'openTypeHheaLineGap', 'openTypeOS2StrikeoutPosition', 'openTypeOS2StrikeoutSize', 'openTypeOS2SubscriptXOffset', 'openTypeOS2SubscriptXSize', 'openTypeOS2SubscriptYOffset', 'openTypeOS2SubscriptYSize', 'openTypeOS2SuperscriptXOffset', 'openTypeOS2SuperscriptXSize', 'openTypeOS2SuperscriptYOffset', 'openTypeOS2SuperscriptYSize', 'openTypeOS2TypoAscender', 'openTypeOS2TypoDescender', 'openTypeOS2TypoLineGap', 'openTypeOS2WeightClass', 'openTypeOS2WidthClass', 'openTypeOS2WinAscent', 'openTypeOS2WinDescent', 'openTypeVheaCaretOffset', 'openTypeVheaCaretSlopeRise', 'openTypeVheaCaretSlopeRun', 'openTypeVheaVertTypoAscender', 'openTypeVheaVertTypoDescender', 'openTypeVheaVertTypoLineGap', 'postscriptBlueFuzz', 'postscriptBlueShift', 'postscriptDefaultWidthX', 'postscriptSlantAngle', 'postscriptUnderlinePosition', 'postscriptUnderlineThickness', 'postscriptUniqueID', 'postscriptWindowsCharacterSet', 'shoulderHeight', 'smallCapHeight', 'typoAscender', 'typoDescender', 'typoLineGap', 'underlinePosition', 'underlineThickness', 'unitsPerEm', 'vheaVertAscender', 'vheaVertDescender', 'vheaVertLineGap', 'weightClass', 'widthClass', 'winAscent', 'winDescent', 'year', 'Grid Spacing')) _CUSTOM_FLOAT_PARAMS = frozenset(( 'postscriptBlueScale',)) _CUSTOM_BOOL_PARAMS = frozenset(( 'isFixedPitch', 'postscriptForceBold', 'postscriptIsFixedPitch', 'Don\u2019t use Production Names', 'DisableAllAutomaticBehaviour', 'Use Typo Metrics', 'Has WWS Names', 'Use Extension Kerning', 'Disable Subroutines', 'Don\'t use Production Names', 'Disable Last Change')) _CUSTOM_INTLIST_PARAMS = frozenset(( 'fsType', 'openTypeOS2CodePageRanges', 'openTypeOS2FamilyClass', 'openTypeOS2Panose', 'openTypeOS2Type', 'openTypeOS2UnicodeRanges', 'panose', 'unicodeRanges', 'codePageRanges', 'openTypeHeadFlags')) _CUSTOM_DICT_PARAMS = frozenset(( 'GASP Table')) def __init__(self, name="New Value", value="New Parameter"): self.name = name self.value = value def __repr__(self): return "<%s %s: %s>" % \ (self.__class__.__name__, self.name, self._value) def plistValue(self): string = UnicodeIO() writer = Writer(string) writer.writeDict({'name': self.name, 'value': self.value}) return string.getvalue() def getValue(self): return self._value def setValue(self, value): """Cast some known data in custom parameters.""" if self.name in self._CUSTOM_INT_PARAMS: value = int(value) elif self.name in self._CUSTOM_FLOAT_PARAMS: value = float(value) elif self.name in self._CUSTOM_BOOL_PARAMS: value = bool(value) elif self.name in self._CUSTOM_INTLIST_PARAMS: value = readIntlist(value) elif self.name in self._CUSTOM_DICT_PARAMS: parser = Parser() value = parser.parse(value) elif self.name == 'note': value = unicode(value) self._value = value value = property(getValue, setValue) class GSAlignmentZone(GSBase): def __init__(self, pos=0, size=20): self.position = pos self.size = size def read(self, src): if src is not None: p = point(src) self.position = float(p.value[0]) self.size = float(p.value[1]) return self def __repr__(self): return "<%s pos:%g size:%g>" % \ (self.__class__.__name__, self.position, self.size) def __lt__(self, other): return (self.position, self.size) < (other.position, other.size) def plistValue(self): return '"{%s, %s}"' % \ (floatToString(self.position), floatToString(self.size)) class GSGuideLine(GSBase): _classesForName = { "alignment": str, "angle": float, "locked": bool, "position": point, "showMeasurement": bool, "filter": str, "name": unicode, } _parent = None _defaultsForName = { "position": point(0, 0), } def __init__(self): super(GSGuideLine, self).__init__() def __repr__(self): return "<%s x=%.1f y=%.1f angle=%.1f>" % \ (self.__class__.__name__, self.position.x, self.position.y, self.angle) @property def parent(self): return self._parent class GSFontMaster(GSBase): _classesForName = { "alignmentZones": GSAlignmentZone, "ascender": float, "capHeight": float, "custom": unicode, "customValue": float, "custom1": unicode, "customValue1": float, "custom2": unicode, "customValue2": float, "custom3": unicode, "customValue3": float, "customParameters": GSCustomParameter, "descender": float, "guideLines": GSGuideLine, "horizontalStems": int, "iconName": str, "id": str, "italicAngle": float, "name": unicode, "userData": dict, "verticalStems": int, "visible": bool, "weight": str, "weightValue": float, "width": str, "widthValue": float, "xHeight": float, } _defaultsForName = { "weightValue": 100.0, "widthValue": 100.0, "xHeight": 500, "capHeight": 700, "ascender": 800, } _wrapperKeysTranslate = { "guideLines": "guides", "custom": "customName", "custom1": "customName1", "custom2": "customName2", "custom3": "customName3", } _keyOrder = ( "alignmentZones", "ascender", "capHeight", "custom", "customValue", "custom1", "customValue1", "custom2", "customValue2", "custom3", "customValue3", "customParameters", "descender", "guideLines", "horizontalStems", "iconName", "id", "italicAngle", "name", "userData", "verticalStems", "visible", "weight", "weightValue", "width", "widthValue", "xHeight" ) def __init__(self): super(GSFontMaster, self).__init__() self.font = None self._name = None self._customParameters = [] self._weight = "Regular" self._width = "Regular" self.italicAngle = 0.0 self._userData = None for number in ('', '1', '2', '3'): setattr(self, 'customName' + number, '') setattr(self, 'customValue' + number, 0.0) def __repr__(self): return '' % \ (self.name, self.widthValue, self.weightValue) def shouldWriteValueForKey(self, key): if key in ("width", "weight"): if getattr(self, key) == "Regular": return False return True if key in ("xHeight", "capHeight", "ascender"): # Always write those values return True if key == "name": if getattr(self, key) == "Regular": return False return True return super(GSFontMaster, self).shouldWriteValueForKey(key) @property def name(self): name = self.customParameters["Master Name"] if name is None: names = [self._weight, self._width] for number in ('', '1', '2', '3'): custom_name = getattr(self, 'customName' + number) if (custom_name and len(custom_name) and custom_name not in names): names.append(custom_name) if len(names) > 1 and "Regular" in names: names.remove("Regular") if abs(self.italicAngle) > 0.01: names.append("Italic") name = " ".join(list(names)) self._name = name return name @name.setter def name(self, value): self._name = value customParameters = property( lambda self: CustomParametersProxy(self), lambda self, value: CustomParametersProxy(self).setter(value)) userData = property( lambda self: UserDataProxy(self), lambda self, value: UserDataProxy(self).setter(value)) @property def weight(self): if self._weight is not None: return self._weight return "Regular" @weight.setter def weight(self, value): self._weight = value @property def width(self): if self._width is not None: return self._width return "Regular" @width.setter def width(self, value): self._width = value class GSNode(GSBase): _PLIST_VALUE_RE = re.compile( '"([-.e\d]+) ([-.e\d]+) (LINE|CURVE|QCURVE|OFFCURVE|n/a)' '(?: (SMOOTH))?(?: (\{.*\}))?"', re.DOTALL) MOVE = "move" LINE = "line" CURVE = "curve" OFFCURVE = "offcurve" QCURVE = "qcurve" _parent = None def __init__(self, position=(0, 0), nodetype=LINE, smooth=False, name=None): self.position = point(position[0], position[1]) self.type = nodetype self.smooth = smooth self._parent = None self._userData = None self.name = name def __repr__(self): content = self.type if self.smooth: content += " smooth" return "<%s %g %g %s>" % \ (self.__class__.__name__, self.position.x, self.position.y, content) userData = property( lambda self: UserDataProxy(self), lambda self, value: UserDataProxy(self).setter(value)) @property def parent(self): return self._parent def plistValue(self): content = self.type.upper() if self.smooth: content += " SMOOTH" if self._userData is not None and len(self._userData) > 0: string = UnicodeIO() writer = Writer(string) writer.writeDict(self._userData) content += ' ' content += self._encode_dict_as_string(string.getvalue()) return '"%s %s %s"' % \ (floatToString(self.position[0]), floatToString(self.position[1]), content) def read(self, line): m = self._PLIST_VALUE_RE.match(line).groups() self.position = point(float(m[0]), float(m[1])) self.type = m[2].lower() self.smooth = bool(m[3]) if m[4] is not None and len(m[4]) > 0: value = self._decode_dict_as_string(m[4]) parser = Parser() self._userData = parser.parse(value) return self @property def name(self): if "name" in self.userData: return self.userData["name"] return None @name.setter def name(self, value): if value is None: if "name" in self.userData: del(self.userData["name"]) else: self.userData["name"] = value @property def index(self): assert self.parent return self.parent.nodes.index(self) @property def nextNode(self): assert self.parent index = self.index if index == (len(self.parent.nodes) - 1): return self.parent.nodes[0] elif index < len(self.parent.nodes): return self.parent.nodes[index + 1] @property def prevNode(self): assert self.parent index = self.index if index == 0: return self.parent.nodes[-1] elif index < len(self.parent.nodes): return self.parent.nodes[index - 1] def makeNodeFirst(self): assert self.parent if self.type == 'offcurve': raise ValueError('Off-curve points cannot become start points.') nodes = self.parent.nodes index = self.index newNodes = nodes[index:len(nodes)] + nodes[0:index] self.parent.nodes = newNodes def toggleConnection(self): self.smooth = not self.smooth # TODO @property def connection(self): raise NotImplementedError # TODO @property def selected(self): raise OnlyInGlyphsAppError def _encode_dict_as_string(self, value): """Takes the PLIST string of a dict, and returns the same string encoded such that it can be included in the string representation of a GSNode.""" # Strip the first and last newlines if value.startswith('{\n'): value = '{' + value[2:] if value.endswith('\n}'): value = value[:-2] + '}' value = value.replace('"', '\\"') value = value.replace('\n', '\\n') return value _ESCAPED_CHAR_RE = re.compile(r'\\(.)') @staticmethod def _unescape_char(m): char = m.group(1) if char == '\\': return '\\' if char == 'n': return '\n' if char == '"': return '"' return m.group(0) def _decode_dict_as_string(self, value): """Reverse function of _encode_string_as_dict""" return self._ESCAPED_CHAR_RE.sub(self._unescape_char, value) class GSPath(GSBase): _classesForName = { "nodes": GSNode, "closed": bool } _defaultsForName = { "closed": True, } _parent = None def __init__(self): self._closed = True self.nodes = [] @property def parent(self): return self._parent def shouldWriteValueForKey(self, key): if key == "closed": return True return super(GSPath, self).shouldWriteValueForKey(key) nodes = property( lambda self: PathNodesProxy(self), lambda self, value: PathNodesProxy(self).setter(value)) @property def segments(self): self._segments = [] self._segmentLength = 0 nodeCount = 0 segmentCount = 0 while nodeCount < len(self.nodes): newSegment = segment() newSegment.parent = self newSegment.index = segmentCount if nodeCount == 0: newSegment.appendNode(self.nodes[-1]) else: newSegment.appendNode(self.nodes[nodeCount-1]) if self.nodes[nodeCount].type == 'offcurve': newSegment.appendNode(self.nodes[nodeCount]) newSegment.appendNode(self.nodes[nodeCount+1]) newSegment.appendNode(self.nodes[nodeCount+2]) nodeCount += 3 elif self.nodes[nodeCount].type == 'line': newSegment.appendNode(self.nodes[nodeCount]) nodeCount += 1 self._segments.append(newSegment) self._segmentLength += 1 segmentCount += 1 self._segments return self._segments @segments.setter def segments(self, value): if type(value) in (list, tuple): self.setSegments(segments) else: raise TypeError def setSegments(self, segments): self.nodes = [] for segment in segments: if len(segment.nodes) == 2 or len(segment.nodes) == 4: self.nodes.extend(segment.nodes[1:]) else: raise ValueError @property def bounds(self): left, bottom, right, top = None, None, None, None for segment in self.segments: newLeft, newBottom, newRight, newTop = segment.bbox() if left is None: left = newLeft else: left = min(left, newLeft) if bottom is None: bottom = newBottom else: bottom = min(bottom, newBottom) if right is None: right = newRight else: right = max(right, newRight) if top is None: top = newTop else: top = max(top, newTop) return rect(point(left, bottom), point(right - left, top - bottom)) @property def direction(self): direction = 0 for i in range(len(self.nodes)): thisNode = self.nodes[i] nextNode = thisNode.nextNode direction += (nextNode.position.x - thisNode.position.x) * (nextNode.position.y + thisNode.position.y) if direction < 0: return -1 else: return 1 @property def selected(self): raise OnlyInGlyphsAppError @property def bezierPath(self): raise OnlyInGlyphsAppError def reverse(self): segments = list(reversed(self.segments)) for s, segment in enumerate(segments): segment.nodes = list(reversed(segment.nodes)) if s == len(segments) - 1: nextSegment = segments[0] else: nextSegment = segments[s+1] if len(segment.nodes) == 2 and segment.nodes[-1].type == 'curve': segment.nodes[-1].type = 'line' nextSegment.nodes[0].type = 'line' elif len(segment.nodes) == 4 and segment.nodes[-1].type == 'line': segment.nodes[-1].type = 'curve' nextSegment.nodes[0].type = 'curve' self.setSegments(segments) # TODO def addNodesAtExtremes(self): raise NotImplementedError # TODO def applyTransform(self, transformationMatrix): raise NotImplementedError # Using both skew values (>0.0) produces different results than Glyphs. # Skewing just on of the two works. # Needs more attention. assert len(transformationMatrix) == 6 for node in self.nodes: transformation = ( Affine.translation(transformationMatrix[4], transformationMatrix[5]) * Affine.scale(transformationMatrix[0], transformationMatrix[3]) * Affine.shear(transformationMatrix[2] * 45.0, transformationMatrix[1] * 45.0) ) x, y = (node.position.x, node.position.y) * transformation node.position.x = x node.position.y = y class segment(list): def appendNode(self, node): if not hasattr(self, 'nodes'): # instead of defining this in __init__(), because I hate super() self.nodes = [] self.nodes.append(node) self.append(point(node.position.x, node.position.y)) @property def nextSegment(self): assert self.parent index = self.index if index == (len(self.parent._segments) - 1): return self.parent._segments[0] elif index < len(self.parent._segments): return self.parent._segments[index + 1] @property def prevSegment(self): assert self.parent index = self.index if index == 0: return self.parent._segments[-1] elif index < len(self.parent._segments): return self.parent._segments[index - 1] def bbox(self): if len(self) == 2: left = min(self[0].x, self[1].x) bottom = min(self[0].y, self[1].y) right = max(self[0].x, self[1].x) top = max(self[0].y, self[1].y) return left, bottom, right, top elif len(self) == 4: left, bottom, right, top = self.bezierMinMax(self[0].x, self[0].y, self[1].x, self[1].y, self[2].x, self[2].y, self[3].x, self[3].y) return left, bottom, right, top else: raise ValueError def bezierMinMax(self, x0, y0, x1, y1, x2, y2, x3, y3): tvalues = [] xvalues = [] yvalues = [] for i in range(2): if i == 0: b = 6 * x0 - 12 * x1 + 6 * x2 a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3 c = 3 * x1 - 3 * x0 else: b = 6 * y0 - 12 * y1 + 6 * y2 a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3 c = 3 * y1 - 3 * y0 if abs(a) < 1e-12: if abs(b) < 1e-12: continue t = -c / b if 0 < t and t < 1: tvalues.append(t) continue b2ac = b * b - 4 * c * a if b2ac < 0: continue sqrtb2ac = math.sqrt(b2ac) t1 = (-b + sqrtb2ac) / (2 * a) if 0 < t1 and t1 < 1: tvalues.append(t1) t2 = (-b - sqrtb2ac) / (2 * a) if 0 < t2 and t2 < 1: tvalues.append(t2) for j in range(len(tvalues) - 1, -1, -1): t = tvalues[j] mt = 1 - t newxValue = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3) if len(xvalues) > 0: xvalues[j] = newxValue else: xvalues.append(newxValue) newyValue = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3) if len(yvalues) > 0: yvalues[j] = newyValue else: yvalues.append(newyValue) xvalues.append(x0) xvalues.append(x3) yvalues.append(y0) yvalues.append(y3) return min(xvalues), min(yvalues), max(xvalues), max(yvalues) class GSComponent(GSBase): _classesForName = { "alignment": int, "anchor": str, "locked": bool, "name": unicode, "piece": dict, "transform": transform, } _wrapperKeysTranslate = { "piece": "smartComponentValues", } _defaultsForName = { "transform": transform(1, 0, 0, 1, 0, 0), } _parent = None # TODO: glyph arg is required def __init__(self, glyph="", offset=(0, 0), scale=(1, 1), transform=None): super(GSComponent, self).__init__() if transform is None: if scale != (1, 1) or offset != (0, 0): xx, yy = scale dx, dy = offset self.transform = transform(xx, 0, 0, yy, dx, dy) else: self.transform = transform if isinstance(glyph, (str, unicode)): self.name = glyph elif isinstance(glyph, GSGlyph): self.name = glyph.name def __repr__(self): return '' % \ (self.name, self.transform[4], self.transform[5]) def shouldWriteValueForKey(self, key): if key == "piece": value = getattr(self, key) return len(value) > 0 return super(GSComponent, self).shouldWriteValueForKey(key) @property def parent(self): return self._parent # .position @property def position(self): return point(self.transform[4], self.transform[5]) @position.setter def position(self, value): self.transform[4] = value[0] self.transform[5] = value[1] # .scale @property def scale(self): self._sX, self._sY, self._R = transformStructToScaleAndRotation(self.transform.value) return (self._sX, self._sY) @scale.setter def scale(self, value): self._sX, self._sY, self._R = transformStructToScaleAndRotation(self.transform.value) if type(value) in [int, float]: self._sX = value self._sY = value elif type(value) in [tuple, list] and len(value) == 2: self._sX, self._sY = value else: raise ValueError self.updateAffineTransform() # .rotation @property def rotation(self): self._sX, self._sY, self._R = transformStructToScaleAndRotation(self.transform.value) return self._R @rotation.setter def rotation(self, value): self._sX, self._sY, self._R = transformStructToScaleAndRotation(self.transform.value) self._R = value self.updateAffineTransform() def updateAffineTransform(self): affine = list(Affine.translation(self.transform[4], self.transform[5]) * Affine.scale(self._sX, self._sY) * Affine.rotation(self._R))[:6] self.transform = transform(affine[0], affine[1], affine[3], affine[4], affine[2], affine[5]) @property def componentName(self): return self.name @componentName.setter def componentName(self, value): self.name = value @property def component(self): return self.parent.parent.parent.glyphs[self.name] @property def layer(self): return self.parent.parent.parent.glyphs[self.name].layers[self.parent.layerId] def applyTransformation(self, x, y): x *= self.scale[0] y *= self.scale[1] x += self.position.x y += self.position.y # TODO: # Integrate rotation return x, y @property def bounds(self): bounds = self.layer.bounds if bounds is not None: left, bottom, width, height = self.layer.bounds right = left + width top = bottom + height left, bottom = self.applyTransformation(left, bottom) right, top = self.applyTransformation(right, top) if left is not None and bottom is not None and right is not None and top is not None: return rect(point(left, bottom), point(right - left, top - bottom)) smartComponentValues = property( lambda self: self.piece, lambda self, value: setattr(self, "piece", value)) class GSSmartComponentAxis(GSBase): _classesForName = { "name": unicode, "bottomName": unicode, "bottomValue": float, "topName": unicode, "topValue": float, } _keyOrder = ( "name", "bottomName", "bottomValue", "topName", "topValue", ) def shouldWriteValueForKey(self, key): if key in ("bottomValue", "topValue"): return True return super(GSSmartComponentAxis, self).shouldWriteValueForKey(key) class GSAnchor(GSBase): _classesForName = { "name": unicode, "position": point, } _parent = None _defaultsForName = { "position": point(0, 0), } def __init__(self, name=None, position=None): super(GSAnchor, self).__init__() if name is not None: self.name = name if position is not None: self.position = position def __repr__(self): return '<%s "%s" x=%.1f y=%.1f>' % \ (self.__class__.__name__, self.name, self.position[0], self.position[1]) @property def parent(self): return self._parent class GSHint(GSBase): _classesForName = { "horizontal": bool, "options": int, # bitfield "origin": point, # Index path to node "other1": point, # Index path to node for third node "other2": point, # Index path to node for fourth node "place": point, # (position, width) "scale": point, # for corners "stem": int, # index of stem "target": hint_target, # Index path to node or 'up'/'down' "type": str, "name": unicode, "settings": dict } _defaultsForName = { "stem": -2, } _keyOrder = ( "horizontal", "origin", "place", "target", "other1", "other2", "scale", "type", "stem", "name", "options", "settings" ) def shouldWriteValueForKey(self, key): if key == "stem": if self.stem == -2: return None if (key in ['origin', 'other1', 'other2', 'place', 'scale'] and getattr(self, key).value == getattr(self, key).default): return None if key == "settings" and (self.settings is None or len(self.settings) == 0): return None return super(GSHint, self).shouldWriteValueForKey(key) def _origin_pos(self): if self.originNode: if self.horizontal: return self.originNode.position.y else: return self.originNode.position.x return self.origin def _width_pos(self): if self.targetNode: if self.horizontal: return self.targetNode.position.y else: return self.targetNode.position.x return self.width def __repr__(self): if self.horizontal: direction = "horizontal" else: direction = "vertical" if self.type == 'BOTTOMGHOST' or self.type == 'TOPGHOST': return "" % (self.type, self._origin_pos()) elif self.type == 'STEM': return "" % ( direction, self._origin_pos(), self._width_pos()) elif self.type == 'CORNER' or self.type == 'CAP': return "" % (self.type, self.name) else: return "" % (self.type, direction) @property def parent(self): return self._parent def _find_node_by_indices(self, point): """"Find the GSNode that is refered to by the given indices.""" path_index, node_index = point layer = self.parent path = layer.paths[int(path_index)] node = path.nodes[int(node_index)] return node def _find_indices_for_node(self, node): """Find the path_index and node_index that identify the given node.""" path = node.parent layer = path.parent for path_index in range(len(layer.paths)): if path == layer.paths[path_index]: for node_index in range(len(path.nodes)): if node == path.nodes[node_index]: return point(path_index, node_index) return None @property def originNode(self): if self._originNode is not None: return self._originNode if self._origin is not None: return self._find_node_by_indices(self._origin) @originNode.setter def originNode(self, node): self._originNode = node self._origin = None @property def origin(self): if self._origin is not None: return self._origin if self._originNode is not None: return self._find_indices_for_node(self._originNode) @origin.setter def origin(self, origin): self._origin = origin self._originNode = None @property def targetNode(self): if self._targetNode is not None: return self._targetNode if self._target is not None: return self._find_node_by_indices(self._target) @targetNode.setter def targetNode(self, node): self._targetNode = node self._target = None @property def target(self): if self._target is not None: return self._target if self._targetNode is not None: return self._find_indices_for_node(self._targetNode) @target.setter def target(self, target): self._target = target self._targetNode = None @property def otherNode1(self): if self._otherNode1 is not None: return self._otherNode1 if self._other1 is not None: return self._find_node_by_indices(self._other1) @otherNode1.setter def otherNode1(self, node): self._otherNode1 = node self._other1 = None @property def other1(self): if self._other1 is not None: return self._other1 if self._otherNode1 is not None: return self._find_indices_for_node(self._otherNode1) @other1.setter def other1(self, other1): self._other1 = other1 self._otherNode1 = None @property def otherNode2(self): if self._otherNode2 is not None: return self._otherNode2 if self._other2 is not None: return self._find_node_by_indices(self._other2) @otherNode2.setter def otherNode2(self, node): self._otherNode2 = node self._other2 = None @property def other2(self): if self._other2 is not None: return self._other2 if self._otherNode2 is not None: return self._find_indices_for_node(self._otherNode2) @other2.setter def other2(self, other2): self._other2 = other2 self._otherNode2 = None class GSFeature(GSBase): _classesForName = { "automatic": bool, "code": unicode, "name": str, "notes": unicode, "disabled": bool, } def __init__(self, name="xxxx", code=""): super(GSFeature, self).__init__() self.name = name self.code = code def shouldWriteValueForKey(self, key): if key == "code": return True return super(GSFeature, self).shouldWriteValueForKey(key) def getCode(self): return self._code def setCode(self, code): replacements = ( ('\\012', '\n'), ('\\011', '\t'), ('\\U2018', "'"), ('\\U2019', "'"), ('\\U201C', '"'), ('\\U201D', '"')) for escaped, unescaped in replacements: code = code.replace(escaped, unescaped) self._code = code code = property(getCode, setCode) def __repr__(self): return '<%s "%s">' % \ (self.__class__.__name__, self.name) @property def parent(self): return self._parent class GSClass(GSFeature): pass class GSFeaturePrefix(GSFeature): pass class GSAnnotation(GSBase): _classesForName = { "angle": float, "position": point, "text": unicode, "type": str, "width": float, # the width of the text field or size of the cicle } _parent = None @property def parent(self): return self._parent class GSInstance(GSBase): _classesForName = { "customParameters": GSCustomParameter, "exports": bool, "instanceInterpolations": dict, "interpolationCustom": float, "interpolationCustom1": float, "interpolationCustom2": float, "interpolationWeight": float, "interpolationWidth": float, "isBold": bool, "isItalic": bool, "linkStyle": str, "manualInterpolation": bool, "name": unicode, "weightClass": str, "widthClass": str, } _defaultsForName = { "exports": True, "interpolationWeight": 100, "interpolationWidth": 100, "weightClass": "Regular", "widthClass": "Medium (normal)", } _keyOrder = ( "exports", "customParameters", "interpolationCustom", "interpolationCustom1", "interpolationCustom2", "interpolationWeight", "interpolationWidth", "instanceInterpolations", "isBold", "isItalic", "linkStyle", "manualInterpolation", "name", "weightClass", "widthClass", ) def interpolateFont(): pass def __init__(self): self.exports = True self.name = "Regular" self.weight = "Regular" self.width = "Regular" self.custom = None self.linkStyle = "" self.interpolationWeight = 100.0 self.interpolationWidth = 100.0 self.interpolationCustom = 0.0 self.visible = True self.isBold = False self.isItalic = False self.widthClass = "Medium (normal)" self.weightClass = "Regular" self._customParameters = [] customParameters = property( lambda self: CustomParametersProxy(self), lambda self, value: CustomParametersProxy(self).setter(value)) weightValue = property( lambda self: self.interpolationWeight, lambda self, value: setattr(self, "interpolationWeight", value)) widthValue = property( lambda self: self.interpolationWidth, lambda self, value: setattr(self, "interpolationWidth", value)) customValue = property( lambda self: self.interpolationCustom, lambda self, value: setattr(self, "interpolationCustom", value)) @property def familyName(self): value = self.customParameters["familyName"] if value: return value return self.parent.familyName @familyName.setter def familyName(self, value): self.customParameters["famiyName"] = value @property def preferredFamily(self): value = self.customParameters["preferredFamily"] if value: return value return self.parent.familyName @preferredFamily.setter def preferredFamily(self, value): self.customParameters["preferredFamily"] = value @property def preferredSubfamilyName(self): value = self.customParameters["preferredSubfamilyName"] if value: return value return self.name @preferredSubfamilyName.setter def preferredSubfamilyName(self, value): self.customParameters["preferredSubfamilyName"] = value @property def windowsFamily(self): value = self.customParameters["styleMapFamilyName"] if value: return value if self.name not in ("Regular", "Bold", "Italic", "Bold Italic"): return self.familyName + " " + self.name else: return self.familyName @windowsFamily.setter def windowsFamily(self, value): self.customParameters["styleMapFamilyName"] = value @property def windowsStyle(self): if self.name in ("Regular", "Bold", "Italic", "Bold Italic"): return self.name else: return "Regular" @property def windowsLinkedToStyle(self): value = self.linkStyle return value if self.name in ("Regular", "Bold", "Italic", "Bold Italic"): return self.name else: return "Regular" @property def fontName(self): value = self.customParameters["postscriptFontName"] if value: return value # TODO: strip invalid characters return "".join(self.familyName.split(" ")) + "-" + self.name @fontName.setter def fontName(self, value): self.customParameters["postscriptFontName"] = value @property def fullName(self): value = self.customParameters["postscriptFullName"] if value: return value return self.familyName + " " + self.name @fullName.setter def fullName(self, value): self.customParameters["postscriptFullName"] = value class GSBackgroundImage(GSBase): _classesForName = { "crop": rect, "imagePath": unicode, "locked": bool, "transform": transform, "alpha": int, } _defaultsForName = { "transform": transform(1, 0, 0, 1, 0, 0), } def __init__(self, path=None): super(GSBackgroundImage, self).__init__() self.imagePath = path self._sX, self._sY, self._R = transformStructToScaleAndRotation(self.transform.value) def __repr__(self): return "" % self.imagePath # .path @property def path(self): return self.imagePath @path.setter def path(self, value): # FIXME: (jany) use posix pathnames here? if os.dirname(os.abspath(value)) == os.dirname(os.abspath(self.parent.parent.parent.filepath)): self.imagePath = os.path.basename(value) else: self.imagePath = value # .position @property def position(self): return point(self.transform[4], self.transform[5]) @position.setter def position(self, value): self.transform[4] = value[0] self.transform[5] = value[1] # .scale @property def scale(self): return (self._sX, self._sY) @scale.setter def scale(self, value): if type(value) in [int, float]: self._sX = value self._sY = value elif type(value) in [tuple, list] and len(value) == 2: self._sX, self._sY = value else: raise ValueError self.updateAffineTransform() # .rotation @property def rotation(self): return self._R @rotation.setter def rotation(self, value): self._R = value self.updateAffineTransform() def updateAffineTransform(self): affine = list(Affine.translation(self.transform[4], self.transform[5]) * Affine.scale(self._sX, self._sY) * Affine.rotation(self._R))[:6] self.transform = [affine[0], affine[1], affine[3], affine[4], affine[2], affine[5]] # FIXME: (jany) This class is not mentioned in the official docs? class GSBackgroundLayer(GSBase): _classesForName = { "anchors": GSAnchor, "annotations": GSAnnotation, "backgroundImage": GSBackgroundImage, "components": GSComponent, "guideLines": GSGuideLine, "hints": GSHint, "paths": GSPath, "visible": bool, } _wrapperKeysTranslate = { "guideLines": "guides", } class GSLayer(GSBase): _classesForName = { "anchors": GSAnchor, "annotations": GSAnnotation, "associatedMasterId": str, "background": GSBackgroundLayer, "backgroundImage": GSBackgroundImage, "color": color, "components": GSComponent, "guideLines": GSGuideLine, "hints": GSHint, "layerId": str, "leftMetricsKey": unicode, "name": unicode, "paths": GSPath, "rightMetricsKey": unicode, "userData": dict, "vertWidth": float, "vertOrigin": float, "visible": bool, "width": float, "widthMetricsKey": unicode, } _defaultsForName = { "weight": 600, "leftMetricsKey": None, "rightMetricsKey": None, "widthMetricsKey": None, } _wrapperKeysTranslate = { "guideLines": "guides", } _keyOrder = ( "anchors", "annotations", "associatedMasterId", "background", "backgroundImage", "color", "components", "guideLines", "hints", "layerId", "leftMetricsKey", "widthMetricsKey", "rightMetricsKey", "name", "paths", "userData", "visible", "vertOrigin", "vertWidth", "width", ) def __init__(self): super(GSLayer, self).__init__() self._anchors = [] self._hints = [] self._annotations = [] self._components = [] self._guides = [] self._paths = [] self._selection = [] self._userData = None def __repr__(self): name = self.name try: # assert self.name name = self.name except: name = 'orphan (n)' try: assert self.parent.name parent = self.parent.name except: parent = 'orphan' return "<%s \"%s\" (%s)>" % (self.__class__.__name__, name, parent) def __lt__(self, other): if self.master and other.master and self.associatedMasterId == self.layerId: return self.master.weightValue < other.master.weightValue or self.master.widthValue < other.master.widthValue @property def master(self): if self.associatedMasterId and self.parent: master = self.parent.parent.masterForId(self.associatedMasterId) return master def shouldWriteValueForKey(self, key): if key == "associatedMasterId": return self.layerId != self.associatedMasterId if key == "name": return (self.name is not None and len(self.name) > 0 and self.layerId != self.associatedMasterId) if key in ("width"): return True return super(GSLayer, self).shouldWriteValueForKey(key) @property def name(self): if (self.associatedMasterId and self.associatedMasterId == self.layerId and self.parent): master = self.parent.parent.masterForId(self.associatedMasterId) if master: return master.name return self._name @name.setter def name(self, value): self._name = value anchors = property( lambda self: LayerAnchorsProxy(self), lambda self, value: LayerAnchorsProxy(self).setter(value)) hints = property( lambda self: LayerHintsProxy(self), lambda self, value: LayerHintsProxy(self).setter(value)) paths = property( lambda self: LayerPathsProxy(self), lambda self, value: LayerPathsProxy(self).setter(value)) components = property( lambda self: LayerComponentsProxy(self), lambda self, value: LayerComponentsProxy(self).setter(value)) guides = property( lambda self: LayerGuideLinesProxy(self), lambda self, value: LayerGuideLinesProxy(self).setter(value)) annotations = property( lambda self: LayerAnnotationProxy(self), lambda self, value: LayerAnnotationProxy(self).setter(value)) userData = property( lambda self: UserDataProxy(self), lambda self, value: UserDataProxy(self).setter(value)) @property def smartComponentPoleMapping(self): if "PartSelection" not in self.userData: self.userData["PartSelection"] = {} return self.userData["PartSelection"] @smartComponentPoleMapping.setter def smartComponentPoleMapping(self, value): self.userData["PartSelection"] = value @property def bounds(self): left, bottom, right, top = None, None, None, None for item in self.paths.values() + self.components.values(): newLeft, newBottom, newWidth, newHeight = item.bounds newRight = newLeft + newWidth newTop = newBottom + newHeight if left is None: left = newLeft else: left = min(left, newLeft) if bottom is None: bottom = newBottom else: bottom = min(bottom, newBottom) if right is None: right = newRight else: right = max(right, newRight) if top is None: top = newTop else: top = max(top, newTop) if left is not None and bottom is not None and right is not None and top is not None: return rect(point(left, bottom), point(right - left, top - bottom)) class GSGlyph(GSBase): _classesForName = { "bottomKerningGroup": str, "bottomMetricsKey": str, "category": str, "color": color, "export": bool, "glyphname": unicode, "lastChange": glyphs_datetime, "layers": GSLayer, "leftKerningGroup": unicode, "leftKerningKey": unicode, "leftMetricsKey": unicode, "note": unicode, "partsSettings": GSSmartComponentAxis, "production": str, "rightKerningGroup": unicode, "rightKerningKey": unicode, "rightMetricsKey": unicode, "script": str, "subCategory": str, "topKerningGroup": str, "topMetricsKey": str, "unicode": unicode, "userData": dict, "vertWidthMetricsKey": str, "widthMetricsKey": unicode, } _wrapperKeysTranslate = { "glyphname": "name", "partsSettings": "smartComponentAxes", } _defaultsForName = { "category": None, "color": None, "export": True, "lastChange": None, "leftKerningGroup": None, "leftMetricsKey": None, "name": None, "note": None, "rightKerningGroup": None, "rightMetricsKey": None, "script": None, "subCategory": None, "userData": None, "widthMetricsKey": None, "unicode": None, } _keyOrder = ( "color", "export", "glyphname", "production", "lastChange", "layers", "leftKerningGroup", "leftMetricsKey", "widthMetricsKey", "vertWidthMetricsKey", "note", "rightKerningGroup", "rightMetricsKey", "topKerningGroup", "topMetricsKey", "bottomKerningGroup", "bottomMetricsKey", "unicode", "script", "category", "subCategory", "userData", "partsSettings", ) def __init__(self, name=None): super(GSGlyph, self).__init__() self._layers = OrderedDict() self.name = name self.parent = None self.export = True self.selected = False self.smartComponentAxes = [] self._userData = None def __repr__(self): return '' % (self.name, len(self.layers)) def shouldWriteValueForKey(self, key): if key in ("script", "category", "subCategory"): return getattr(self, key) is not None return super(GSGlyph, self).shouldWriteValueForKey(key) layers = property(lambda self: GlyphLayerProxy(self), lambda self, value: GlyphLayerProxy(self).setter(value)) def _setupLayer(self, layer, key): assert type(key) == str layer.parent = self layer.layerId = key # TODO use proxy `self.parent.masters[key]` if self.parent and self.parent.masterForId(key): layer.associatedMasterId = key # def setLayerForKey(self, layer, key): # if Layer and Key: # Layer.parent = self # Layer.layerId = Key # if self.parent.fontMasterForId(Key): # Layer.associatedMasterId = Key # self._layers[key] = layer def removeLayerForKey_(self, key): for layer in list(self._layers): if layer == key: del self._layers[key] @property def string(self): if self.unicode: return unichr(int(self.unicode, 16)) userData = property( lambda self: UserDataProxy(self), lambda self, value: UserDataProxy(self).setter(value)) glyphname = property( lambda self: self.name, lambda self, value: setattr(self, "name", value)) smartComponentAxes = property( lambda self: self.partsSettings, lambda self, value: setattr(self, "partsSettings", value)) @property def id(self): """An unique identifier for each glyph""" return self.name class GSFont(GSBase): _classesForName = { ".appVersion": str, "DisplayStrings": unicode, "classes": GSClass, "copyright": unicode, "customParameters": GSCustomParameter, "date": glyphs_datetime, "designer": unicode, "designerURL": unicode, "disablesAutomaticAlignment": bool, "disablesNiceNames": bool, "familyName": unicode, "featurePrefixes": GSFeaturePrefix, "features": GSFeature, "fontMaster": GSFontMaster, "glyphs": GSGlyph, "gridLength": int, "gridSubDivision": int, "instances": GSInstance, "keepAlternatesTogether": bool, "kerning": OrderedDict, "keyboardIncrement": float, "manufacturer": unicode, "manufacturerURL": unicode, "unitsPerEm": int, "userData": dict, "versionMajor": int, "versionMinor": int, } _wrapperKeysTranslate = { ".appVersion": "appVersion", "fontMaster": "masters", "unitsPerEm": "upm", "gridLength": "grid", "gridSubDivision": "gridSubDivisions" } _defaultsForName = { "classes": [], "customParameters": [], "disablesAutomaticAlignment": False, "disablesNiceNames": False, "gridLength": 1, "gridSubDivision": 1, "unitsPerEm": 1000, "kerning": OrderedDict(), "keyboardIncrement": 1, } def __init__(self, path=None): super(GSFont, self).__init__() self.familyName = "Unnamed font" self._versionMinor = 0 self.versionMajor = 1 self.appVersion = "895" # minimum required version self._glyphs = [] self._masters = [] self._instances = [] self._customParameters = [] self._classes = [] self.filepath = None self._userData = None if path: assert isinstance(path, (str, unicode)), \ "Please supply a file path" assert path.endswith(".glyphs"), \ "Please supply a file path to a .glyphs file" with open(path, 'r', encoding='utf-8') as fp: p = Parser() logger.info('Parsing .glyphs file into %r', self) p.parse_into_object(self, fp.read()) self.filepath = path for master in self.masters: master.font = self def __repr__(self): return "<%s \"%s\">" % (self.__class__.__name__, self.familyName) def shouldWriteValueForKey(self, key): if key in ("unitsPerEm", "versionMinor"): return True return super(GSFont, self).shouldWriteValueForKey(key) def save(self, path=None): if path is None: if self.filepath: path = self.filepath else: raise ValueError("No path provided and GSFont has no filepath") with open(path, 'w', encoding='utf-8') as fp: w = Writer(fp) logger.info('Writing %r to .glyphs file', self) w.write(self) def getVersionMinor(self): return self._versionMinor def setVersionMinor(self, value): """Ensure that the minor version number is between 0 and 999.""" assert value >= 0 and value <= 999 self._versionMinor = value versionMinor = property(getVersionMinor, setVersionMinor) glyphs = property(lambda self: FontGlyphsProxy(self), lambda self, value: FontGlyphsProxy(self).setter(value)) def _setupGlyph(self, glyph): glyph.parent = self for layer in glyph.layers: if (not hasattr(layer, "associatedMasterId") or layer.associatedMasterId is None or len(layer.associatedMasterId) == 0): glyph._setupLayer(layer, layer.layerId) @property def features(self): return self._features @features.setter def features(self, value): self._features = value for g in self._features: g._parent = self masters = property(lambda self: FontFontMasterProxy(self), lambda self, value: FontFontMasterProxy(self).setter(value)) def masterForId(self, key): for master in self._masters: if master.id == key: return master return None @property def instances(self): return self._instances @instances.setter def instances(self, value): self._instances = value for i in self._instances: i.parent = self classes = property( lambda self: FontClassesProxy(self), lambda self, value: FontClassesProxy(self).setter(value)) customParameters = property( lambda self: CustomParametersProxy(self), lambda self, value: CustomParametersProxy(self).setter(value)) userData = property( lambda self: UserDataProxy(self), lambda self, value: UserDataProxy(self).setter(value)) @property def kerning(self): return self._kerning @kerning.setter def kerning(self, kerning): self._kerning = kerning for master_id, master_map in kerning.items(): for left_glyph, glyph_map in master_map.items(): for right_glyph, value in glyph_map.items(): glyph_map[right_glyph] = float(value) @property def selection(self): return (glyph for glyph in self.glyphs if glyph.selected) @property def note(self): value = self.customParameters["note"] if value: return value else: return "" @note.setter def note(self, value): self.customParameters["note"] = value @property def gridLength(self): if (self.gridSubDivisions > 0): return self.grid / self.gridSubDivisions else: return self.grid glyphslib-2.2.1/Lib/glyphsLib/glyphdata.py000066400000000000000000000070121322341616200205030ustar00rootroot00000000000000# -*- coding=utf-8 -*- # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from collections import namedtuple from fontTools import agl from fontTools.misc.py23 import unichr from glyphsLib import glyphdata_generated import sys import struct import unicodedata NARROW_PYTHON_BUILD = sys.maxunicode < 0x10FFFF Glyph = namedtuple("Glyph", "name,production_name,unicode,category,subCategory") def get_glyph(name, data=glyphdata_generated): prodname = data.PRODUCTION_NAMES.get(name) # Some Glyphs files use production names (instead of Glyphs names). # We catch this here, so that we can return the same properties as if # the Glyphs file had been following the Glyphs naming conventions. # https://github.com/googlei18n/glyphsLib/issues/232 if prodname is None: rev_prodname = data.PRODUCTION_NAMES_REVERSED.get(name) if rev_prodname is not None: prodname = name name = rev_prodname if prodname is None: prodname = name unistr = data.IRREGULAR_UNICODE_STRINGS.get(name) if unistr is None: unistr = agl.toUnicode(prodname) if unistr != "" and name not in data.MISSING_UNICODE_STRINGS: unistr_result = unistr else: unistr_result = None category, subCategory = _get_category(name, unistr, data) return Glyph(name, prodname, unistr_result, category, subCategory) def _get_unicode_category(unistr): # We use data for a fixed Unicode version (3.2) so that our generated # data files are independent of Python runtime that runs the rules. # By switching to current Unicode data, we could save some entries # in our exception tables, but the gains are not very large; only # about one thousand entries. if not unistr: return None if NARROW_PYTHON_BUILD: utf32_str = unistr.encode("utf-32-be") nchars = len(utf32_str)//4 first_char = unichr(struct.unpack('>%dL' % nchars, utf32_str)[0]) else: first_char = unistr[0] return unicodedata.ucd_3_2_0.category(first_char) def _get_category(name, unistr, data=glyphdata_generated): cat = data.IRREGULAR_CATEGORIES.get(name) if cat is not None: return cat basename = name.split(".", 1)[0] # "A.alt27" --> "A" if not basename: # handle ".notdef", ".null" basename = name cat = data.IRREGULAR_CATEGORIES.get(basename) if cat is not None: return cat if basename.endswith("-ko"): return ("Letter", "Syllable") if basename.endswith("-ethiopic") or basename.endswith("-tifi"): return ("Letter", None) if basename.startswith("box"): return ("Symbol", "Geometry") if basename.startswith("uniF9"): return ("Letter", "Compatibility") ucat = _get_unicode_category(unistr) cat = data.DEFAULT_CATEGORIES.get(ucat, (None, None)) if "_" in basename: return (cat[0], "Ligature") return cat glyphslib-2.2.1/Lib/glyphsLib/interpolation.py000066400000000000000000000353101322341616200214170ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from collections import OrderedDict, namedtuple import logging import os import xml.etree.ElementTree as etree from glyphsLib.builder.custom_params import set_custom_params from glyphsLib.builder.names import build_stylemap_names from glyphsLib.builder.constants import GLYPHS_PREFIX from glyphsLib.util import build_ufo_path, write_ufo, clean_ufo __all__ = [ 'interpolate', 'build_designspace', 'apply_instance_data' ] logger = logging.getLogger(__name__) # Glyphs.app's default values for the masters' {weight,width,custom}Value # and for the instances' interpolation{Weight,Width,Custom} properties. # When these values are set, they are omitted from the .glyphs source file. DEFAULT_LOCS = { 'weight': 100, 'width': 100, 'custom': 0, } WEIGHT_CODES = { 'Thin': 250, 'ExtraLight': 250, 'UltraLight': 250, 'Light': 300, None: 400, # default value normally omitted in source 'Normal': 400, 'Regular': 400, 'Medium': 500, 'DemiBold': 600, 'SemiBold': 600, 'Bold': 700, 'UltraBold': 800, 'ExtraBold': 800, 'Black': 900, 'Heavy': 900, } WIDTH_CODES = { 'Ultra Condensed': 1, 'Extra Condensed': 2, 'Condensed': 3, 'SemiCondensed': 4, None: 5, # default value normally omitted in source 'Medium (normal)': 5, 'Semi Expanded': 6, 'Expanded': 7, 'Extra Expanded': 8, 'Ultra Expanded': 9, } def interpolate(ufos, master_dir, out_dir, instance_data, round_geometry=True): """Create MutatorMath designspace and generate instances. Returns instance UFOs. """ from mutatorMath.ufo import build designspace_path, instance_files = build_designspace( ufos, master_dir, out_dir, instance_data) logger.info('Building instances') for path, _ in instance_files: clean_ufo(path) build(designspace_path, outputUFOFormatVersion=3, roundGeometry=round_geometry) instance_ufos = apply_instance_data(instance_files) return instance_ufos def build_designspace(masters, master_dir, out_dir, instance_data): """Just create MutatorMath designspace without generating instances. Returns the path of the resulting designspace document and a list of (instance_path, instance_data) tuples which map instance UFO filenames to Glyphs data for that instance. """ from mutatorMath.ufo.document import DesignSpaceDocumentWriter base_family = masters[0].info.familyName assert all(m.info.familyName == base_family for m in masters), \ 'Masters must all have same family' for font in masters: write_ufo(font, master_dir) # needed so that added masters and instances have correct relative paths tmp_path = os.path.join(master_dir, 'tmp.designspace') writer = DesignSpaceDocumentWriter(tmp_path) instances = list(filter(is_instance_active, instance_data.get('data', []))) regular = find_regular_master( masters=masters, regularName=instance_data.get('Variation Font Origin')) axes = get_axes(masters, regular, instances) write_axes(axes, writer) add_masters_to_writer(masters, regular, axes, writer) instance_files = add_instances_to_writer( writer, base_family, axes, instances, out_dir) # append base style shared by all masters to designspace file name base_style = find_base_style(masters) if base_style: base_style = "-" + base_style ds_name = (base_family + base_style).replace(' ', '') + '.designspace' writer.path = os.path.join(master_dir, ds_name) writer.save() return writer.path, instance_files # TODO: Use AxisDescriptor from fonttools once designSpaceDocument has been # made part of fonttools. https://github.com/fonttools/fonttools/issues/911 # https://github.com/LettError/designSpaceDocument#axisdescriptor-object AxisDescriptor = namedtuple('AxisDescriptor', [ 'minimum', 'maximum', 'default', 'name', 'tag', 'labelNames', 'map']) def get_axes(masters, regular_master, instances): # According to Georg Seifert, Glyphs 3 will have a better model # for describing variation axes. The plan is to store the axis # information globally in the Glyphs file. In addition to actual # variation axes, this new structure will probably also contain # stylistic information for design axes that are not variable but # should still be stored into the OpenType STAT table. # # We currently take the minima and maxima from the instances, and # have hard-coded the default value for each axis. We could be # smarter: for the minima and maxima, we could look at the masters # (whose locations are only stored in interpolation space, not in # user space) and reverse-interpolate these locations to user space. # Likewise, we could try to infer the default axis value from the # masters. But it's probably not worth this effort, given that # the upcoming version of Glyphs is going to store explicit # axis desriptions in its file format. axes = OrderedDict() for name, tag, userLocParam, defaultUserLoc in ( ('weight', 'wght', 'weightClass', 400), ('width', 'wdth', 'widthClass', 100), ('custom', 'XXXX', None, 0)): key = GLYPHS_PREFIX + name + 'Value' interpolLocKey = 'interpolation' + name.title() if any(key in master.lib for master in masters): regularInterpolLoc = regular_master.lib.get(key, DEFAULT_LOCS[name]) regularUserLoc = defaultUserLoc labelNames = {"en": name.title()} mapping = [] for instance in instances: interpolLoc = getattr(instance, interpolLocKey, DEFAULT_LOCS[name]) userLoc = interpolLoc for param in instance.customParameters: if param.name == userLocParam: userLoc = float(getattr(param, 'value', DEFAULT_LOCS[name])) break mapping.append((userLoc, interpolLoc)) if interpolLoc == regularInterpolLoc: regularUserLoc = userLoc mapping = sorted(set(mapping)) # avoid duplicates if mapping: minimum = min([userLoc for userLoc, _ in mapping]) maximum = max([userLoc for userLoc, _ in mapping]) default = min(maximum, max(minimum, regularUserLoc)) # clamp else: minimum = maximum = default = defaultUserLoc axes[name] = AxisDescriptor( minimum=minimum, maximum=maximum, default=default, name=name, tag=tag, labelNames=labelNames, map=mapping) return axes def is_instance_active(instance): # Glyphs.app recognizes both "exports=0" and "active=0" as a flag # to mark instances as inactive. Inactive instances should get ignored. # https://github.com/googlei18n/glyphsLib/issues/129 return instance.exports and getattr(instance, 'active', True) def write_axes(axes, writer): # TODO: MutatorMath's DesignSpaceDocumentWriter does not support # axis label names. Once DesignSpaceDocument has been made part # of fonttools, we can write them out in a less hacky way than here. # The current implementation is rather terrible, but it works; # extending the writer isn't worth the effort because we'll move away # from it as soon as DesignSpaceDocument has landed in fonttools. # https://github.com/fonttools/fonttools/issues/911 for axis in axes.values(): writer.addAxis(tag=axis.tag, name=axis.name, minimum=axis.minimum, maximum=axis.maximum, default=axis.default, warpMap=axis.map) axisElement = writer.root.findall('.axes/axis')[-1] for lang, name in sorted(axis.labelNames.items()): labelname = etree.Element('labelname') labelname.attrib['xml:lang'], labelname.text = lang, name axisElement.append(labelname) def find_base_style(masters): """Find a base style shared between all masters. Return empty string if none is found. """ base_style = masters[0].info.styleName.split() for font in masters: style = font.info.styleName.split() base_style = [s for s in style if s in base_style] base_style = ' '.join(base_style) return base_style def find_regular_master(masters, regularName=None): """Find the "regular" master among the master UFOs. Tries to find the master with the passed 'regularName'. If there is no such master or if regularName is None, tries to find a base style shared between all masters (defaulting to "Regular"), and then tries to find a master with that style name. If there is no master with that name, returns the first master in the list. """ assert len(masters) > 0 if regularName is not None: for font in masters: if font.info.styleName == regularName: return font base_style = find_base_style(masters) if not base_style: base_style = 'Regular' for font in masters: if font.info.styleName == base_style: return font return masters[0] def add_masters_to_writer(ufos, regular, axes, writer): """Add master UFOs to a MutatorMath document writer. """ for font in ufos: family, style = font.info.familyName, font.info.styleName # MutatorMath.DesignSpaceDocumentWriter iterates over the location # dictionary, which is non-deterministic so it can cause test failures. # We therefore use an OrderedDict to which we insert in axis order. # Since glyphsLib will switch to DesignSpaceDocument once that is # integrated into fonttools, it's not worth fixing upstream. # https://github.com/googlei18n/glyphsLib/issues/165 location = OrderedDict() for axis in axes: location[axis] = font.lib.get( GLYPHS_PREFIX + axis + 'Value', DEFAULT_LOCS[axis]) is_regular = (font is regular) writer.addSource( path=font.path, name='%s %s' % (family, style), familyName=family, styleName=style, location=location, copyFeatures=is_regular, copyGroups=is_regular, copyInfo=is_regular, copyLib=is_regular) def add_instances_to_writer(writer, family_name, axes, instances, out_dir): """Add instances from Glyphs data to a MutatorMath document writer. Returns a list of pairs, corresponding to the instances which will be output by the document writer. The font data is the Glyphs data for this instance as a dict. """ ofiles = [] for instance in instances: familyName, postScriptFontName, ufo_path = None, None, None for p in instance.customParameters: param, value = p.name, p.value if param == 'familyName': familyName = value elif param == 'postscriptFontName': # Glyphs uses "postscriptFontName", not "postScriptFontName" postScriptFontName = value elif param == 'fileName': ufo_path = os.path.join(out_dir, value + '.ufo') if familyName is None: familyName = family_name styleName = instance.name if not ufo_path: ufo_path = build_ufo_path(out_dir, familyName, styleName) ofiles.append((ufo_path, instance)) # MutatorMath.DesignSpaceDocumentWriter iterates over the location # dictionary, which is non-deterministic so it can cause test failures. # We therefore use an OrderedDict to which we insert in axis order. # Since glyphsLib will switch to DesignSpaceDocument once that is # integrated into fonttools, it's not worth fixing upstream. # https://github.com/googlei18n/glyphsLib/issues/165 location = OrderedDict() for axis in axes: location[axis] = getattr( instance, 'interpolation' + axis.title(), DEFAULT_LOCS[axis]) styleMapFamilyName, styleMapStyleName = build_stylemap_names( family_name=familyName, style_name=styleName, is_bold=instance.isBold, is_italic=instance.isItalic, linked_style=instance.linkStyle, ) writer.startInstance( name=' '.join((familyName, styleName)), location=location, familyName=familyName, styleName=styleName, postScriptFontName=postScriptFontName, styleMapFamilyName=styleMapFamilyName, styleMapStyleName=styleMapStyleName, fileName=ufo_path) writer.writeInfo() writer.writeKerning() writer.endInstance() return ofiles def _set_class_from_instance(ufo, data, key, codes): class_name = getattr(data, key) if class_name: ufo.lib[GLYPHS_PREFIX + key] = class_name if class_name in codes: class_code = codes[class_name] ufo_key = "".join(['openTypeOS2', key[0].upper(), key[1:]]) setattr(ufo.info, ufo_key, class_code) def set_weight_class(ufo, instance_data): """ Store `weightClass` instance attributes in the UFO lib, and set the ufo.info.openTypeOS2WeightClass accordingly. """ _set_class_from_instance(ufo, instance_data, "weightClass", WEIGHT_CODES) def set_width_class(ufo, instance_data): """ Store `widthClass` instance attributes in the UFO lib, and set the ufo.info.openTypeOS2WidthClass accordingly. """ _set_class_from_instance(ufo, instance_data, "widthClass", WIDTH_CODES) def apply_instance_data(instance_data): """Open instances, apply data, and re-save. Args: instance_data: List of (path, data) tuples, one for each instance. Returns: List of opened and updated instance UFOs. """ from defcon import Font instance_ufos = [] for path, data in instance_data: ufo = Font(path) set_weight_class(ufo, data) set_width_class(ufo, data) set_custom_params(ufo, data=data) ufo.save() instance_ufos.append(ufo) return instance_ufos glyphslib-2.2.1/Lib/glyphsLib/parser.py000066400000000000000000000201401322341616200200170ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from fontTools.misc.py23 import tounicode, unichr, unicode from collections import OrderedDict from io import open import re import logging import sys import glyphsLib logger = logging.getLogger(__name__) class Parser(object): """Parses Python dictionaries from Glyphs source files.""" value_re = r'(".*?(?" % (self.__class__.__name__, self.plistValue()) def read(self, src): """Return a typed value representing the structured glyphs strings.""" raise NotImplementedError('%s read' % type(self).__name__) def plistValue(self): """Return structured glyphs strings representing the typed value.""" raise NotImplementedError('%s write' % type(self).__name__) class point(object): """Read/write a vector in curly braces.""" dimension = 2 default = [None, None] regex = re.compile('{%s}' % ', '.join(['([-.e\\d]+)'] * dimension)) def __init__(self, value=None, value2=None, rect=None): if value is not None and value2 is not None: self.value = [value, value2] elif value is not None and value2 is None: value = value.replace('"', '') self.value = [float(i) for i in self.regex.match(value).groups()] else: self.value = self.default self.rect = rect def __repr__(self): return '' % (self.value[0], self.value[1]) def plistValue(self): assert (isinstance(self.value, list) and len(self.value) == self.dimension) if self.value is not self.default: return '"{%s}"' % (', '.join(floatToString(v, 3) for v in self.value)) def __getitem__(self, key): if type(key) is int and key < self.dimension: if key < len(self.value): return self.value[key] else: return 0 else: raise IndexError def __setitem__(self, key, value): if type(key) is int and key < self.dimension: while self.dimension > len(self.value): self.value.append(0) self.value[key] = value else: raise IndexError def __len__(self): return self.dimension @property def x(self): return self.value[0] @x.setter def x(self, value): self.value[0] = value # Update parent rect if self.rect: self.rect.value[0] = value @property def y(self): return self.value[1] @y.setter def y(self, value): self.value[1] = value # Update parent rect if self.rect: self.rect.value[1] = value class size(point): def __repr__(self): return '' % (self.value[0], self.value[1]) @property def width(self): return self.value[0] @width.setter def width(self, value): self.value[0] = value # Update parent rect if self.rect: self.rect.value[2] = value @property def height(self): return self.value[1] @height.setter def height(self, value): self.value[1] = value # Update parent rect if self.rect: self.rect.value[3] = value class rect(object): """Read/write a rect of two points in curly braces.""" #crop = "{{0, 0}, {427, 259}}"; dimension = 4 default = [0, 0, 0, 0] regex = re.compile('{{([-.e\d]+), ([-.e\d]+)}, {([-.e\d]+), ([-.e\d]+)}}') def __init__(self, value = None, value2 = None): if value is not None and value2 is not None: self.value = [value[0], value[1], value2[0], value2[1]] elif value is not None and value2 is None: value = value.replace('"', '') self.value = [float(i) for i in self.regex.match(value).groups()] else: self.value = self.default def plistValue(self): assert isinstance(self.value, list) and len(self.value) == self.dimension return '"{{%s, %s}, {%s, %s}}"' % (floatToString(self.value[0], 3), floatToString(self.value[1], 3), floatToString(self.value[2], 3), floatToString(self.value[3], 3)) def __repr__(self): return '' % (str(self.origin), str(self.size)) def __getitem__(self, key): return self.value[key] def __setitem__(self, key, value): if type(key) is int and key < self.dimension: while self.dimension > len(self.value): self.value.append(0) self.value[key] = value else: raise KeyError def __len__(self): return self.dimension @property def origin(self): return point(self.value[0], self.value[1], rect = self) @origin.setter def origin(self, value): self.value[0] = value.x self.value[1] = value.y @property def size(self): return size(self.value[2], self.value[3], rect = self) @size.setter def size(self, value): self.value[2] = value.width self.value[3] = value.height class transform(point): """Read/write a six-element vector.""" dimension = 6 default = [None, None, None, None, None, None] regex = re.compile('{%s}' % ', '.join(['([-.e\d]+)'] * dimension)) def __init__(self, value = None, value2 = None, value3 = None, value4 = None, value5 = None, value6 = None): if value is not None and value2 is not None and value3 is not None and value4 is not None and value5 is not None and value6 is not None: self.value = [value, value2, value3, value4, value5, value6] elif value is not None and value2 is None: value = value.replace('"', '') self.value = [float(i) for i in self.regex.match(value).groups()] else: self.value = self.default def __repr__(self): return '' % (' '.join(map(str, self.value))) def plistValue(self): assert (isinstance(self.value, list) and len(self.value) == self.dimension) return '"{%s}"' % (', '.join(floatToString(v, 5) for v in self.value)) class glyphs_datetime(baseType): """Read/write a datetime. Doesn't maintain time zone offset.""" utc_offset_re = re.compile( r".* (?P\+|\-)(?P\d\d)(?P\d\d)$") def read(self, src): """Parse a datetime object from a string.""" string = src.replace('"', '') # parse timezone ourselves, since %z is not always supported # see: http://bugs.python.org/issue6641 m = glyphs_datetime.utc_offset_re.match(string) if m: sign = 1 if m.group("sign") == "+" else -1 tz_hours = sign * int(m.group("hours")) tz_minutes = sign * int(m.group("minutes")) offset = datetime.timedelta(hours=tz_hours, minutes=tz_minutes) string = string[:-6] else: # no explicit timezone offset = datetime.timedelta(0) if 'AM' in string or 'PM' in string: datetime_obj = datetime.datetime.strptime( string, '%Y-%m-%d %I:%M:%S %p' ) else: datetime_obj = datetime.datetime.strptime( string, '%Y-%m-%d %H:%M:%S' ) return datetime_obj + offset def plistValue(self): return "\"%s +0000\"" % self.value def strftime(self, val): try: return self.value.strftime(val) except: return None class color(baseType): def read(self, src=None): src.replace('"', '') if src is None: return None if src[0] == "(": src = src[1:-1] color = src.split(",") color = tuple([int(c) for c in color]) else: color = int(src) return color def __repr__(self): return self.value.__repr__() def plistValue(self): if self.value is not None: return str(self.value) return None # mutate list in place def _mutate_list(fn, l): assert isinstance(l, list) for i in range(len(l)): l[i] = fn(l[i]) return l def readIntlist(src): return _mutate_list(int, src) def writeIntlist(val): return _mutate_list(str, val) def actualPrecition(Float): ActualPrecition = 5 Integer = round(Float * 100000.0) while ActualPrecition >= 0: if Integer != round(Integer / 10.0) * 10: return ActualPrecition Integer = round(Integer / 10.0) ActualPrecition -= 1 if ActualPrecition < 0: ActualPrecition = 0 return ActualPrecition def floatToString(Float, precision=3): try: ActualPrecition = actualPrecition(Float) precision = min(precision, ActualPrecition) fractional = math.modf(math.fabs(Float))[0] if precision >= 5 and fractional >= 0.000005 and fractional <= 0.999995: return "%.5f" % Float elif precision >= 4 and fractional >= 0.00005 and fractional <= 0.99995: return "%.4f" % Float elif precision >= 3 and fractional >= 0.0005 and fractional <= 0.9995: return "%.3f" % Float elif precision >= 2 and fractional >= 0.005 and fractional <= 0.995: return "%.2f" % Float elif precision >= 1 and fractional >= 0.05 and fractional <= 0.95: return "%.1f" % Float else: return "%.0f" % Float except: print(traceback.format_exc()) glyphslib-2.2.1/Lib/glyphsLib/util.py000066400000000000000000000037241322341616200175110ustar00rootroot00000000000000# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import os import shutil from fontTools.misc.textTools import num2binary logger = logging.getLogger(__name__) def build_ufo_path(out_dir, family_name, style_name): """Build string to use as a UFO path.""" return os.path.join( out_dir, '%s-%s.ufo' % ( family_name.replace(' ', ''), style_name.replace(' ', ''))) def write_ufo(ufo, out_dir): """Write a UFO.""" out_path = build_ufo_path( out_dir, ufo.info.familyName, ufo.info.styleName) logger.info('Writing %s' % out_path) clean_ufo(out_path) ufo.save(out_path) def clean_ufo(path): """Make sure old UFO data is removed, as it may contain deleted glyphs.""" if path.endswith('.ufo') and os.path.exists(path): shutil.rmtree(path) def cast_to_number_or_bool(inputstr): """Cast a string to int, float or bool. Return original string if it can't be converted. Scientific expression is converted into float. """ if inputstr.strip().lower() == 'true': return True elif inputstr.strip().lower() == 'false': return False try: return int(inputstr) except ValueError: try: return float(inputstr) except ValueError: return inputstr def bin_to_int_list(value): string = num2binary(value) return [i for i, v in enumerate(reversed(string)) if v == "1"] glyphslib-2.2.1/Lib/glyphsLib/writer.py000066400000000000000000000157071322341616200200540ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright 2016 Georg Seifert. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http: #www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import unicode_literals import sys import glyphsLib.classes from glyphsLib.types import floatToString import logging import datetime from collections import OrderedDict from fontTools.misc.py23 import unicode, open, BytesIO, UnicodeIO ''' Usage >> fp = open('Path/to/File.glyphs', 'w') >> writer = Writer(fp) >> writer.write(font) >> fp.close() ''' logger = logging.getLogger(__name__) class Writer(object): def __init__(self, fp): # figure out whether file object expects bytes or unicodes try: fp.write(b'') except TypeError: fp.write(u'') # this better not fail... # file already accepts unicodes; use it directly self.file = fp else: # file expects bytes; wrap it in a UTF-8 codecs.StreamWriter import codecs self.file = codecs.getwriter('utf-8')(fp) def write(self, rootObject): self.writeDict(rootObject) self.file.write("\n") def writeDict(self, dictValue): self.file.write("{\n") forType = None if hasattr(dictValue, "_keyOrder"): keys = dictValue._keyOrder elif hasattr(dictValue, "_classesForName"): keys = sorted(dictValue._classesForName.keys()) else: keys = dictValue.keys() if not isinstance(dictValue, OrderedDict): keys = sorted(keys) for key in keys: if hasattr(dictValue, "_classesForName"): forType = dictValue._classesForName[key] try: if isinstance(dictValue, (dict, OrderedDict)): value = dictValue[key] else: getKey = key if hasattr(dictValue, "_wrapperKeysTranslate"): getKey = dictValue._wrapperKeysTranslate.get(key, key) value = getattr(dictValue, getKey) except AttributeError: continue if value is None: continue if (hasattr(dictValue, "shouldWriteValueForKey") and not dictValue.shouldWriteValueForKey(key)): continue self.writeKey(key) self.writeValue(value, key, forType=forType) self.file.write(";\n") self.file.write("}") def writeArray(self, arrayValue): self.file.write("(\n") idx = 0 length = len(arrayValue) if hasattr(arrayValue, "plistArray"): arrayValue = arrayValue.plistArray() for value in arrayValue: self.writeValue(value) if idx < length - 1: self.file.write(",\n") else: self.file.write("\n") idx += 1 self.file.write(")") def writeUserData(self, userDataValue): self.file.write("{\n") keys = sorted(userDataValue.keys()) for key in keys: value = userDataValue[key] self.writeKey(key) self.writeValue(value, key) self.file.write(";\n") self.file.write("}") def writeValue(self, value, forKey=None, forType=None): if isinstance(value, (list, glyphsLib.classes.Proxy)): if isinstance(value, glyphsLib.classes.UserDataProxy): self.writeUserData(value) else: self.writeArray(value) elif hasattr(value, "plistValue"): value = value.plistValue() if value is not None: self.file.write(value) elif isinstance(value, (dict, OrderedDict, glyphsLib.classes.GSBase)): self.writeDict(value) elif type(value) == float: self.file.write(floatToString(value, 5)) elif type(value) == int: self.file.write(unicode(value)) elif type(value) == bool: if value: self.file.write("1") else: self.file.write("0") elif type(value) == datetime.datetime: self.file.write("\"%s +0000\"" % str(value)) else: value = unicode(value) if forKey != "unicode": value = escape_string(value) self.file.write(value) def writeKey(self, key): key = escape_string(key) self.file.write("%s = " % key) def dump(obj, fp): """Write a GSFont object to a .glyphs file. 'fp' should be a (writable) file object. """ writer = Writer(fp) logger.info('Writing .glyphs file') writer.write(obj) def dumps(obj): """Serialize a GSFont object to a .glyphs file format. Return a (unicode) str object. """ fp = UnicodeIO() dump(obj, fp) return fp.getvalue() NSPropertyListNameSet = ( # 0 False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, # 16 False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, # 32 False, False, False, False, True, False, False, False, False, False, False, False, False, False, True, False, # 48 True, True, True, True, True, True, True, True, True, True, False, False, False, False, False, False, # 64 False, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, # 80 True, True, True, True, True, True, True, True, True, True, True, False, False, False, False, True, # 96 False, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, # 112 True, True, True, True, True, True, True, True, True, True, True, False, False, False, False, False ) def _needs_quotes(string): if len(string) == 0: return True # Does it need quotes because of special characters? for c in string: d = ord(c) if d >= 128 or not NSPropertyListNameSet[d]: return True # Does it need quotes because it could be confused with a number? try: int(string) except ValueError: return False else: return True def escape_string(string): if _needs_quotes(string): string = string.replace("\\", "\\\\") string = string.replace("\"", "\\\"") string = string.replace("\n", "\\012") string = '"%s"' % string return string glyphslib-2.2.1/MANIFEST.in000066400000000000000000000002211322341616200152420ustar00rootroot00000000000000include README.rst include CONTRIBUTING.md include LICENSE include requirements.txt include tox.ini recursive-include tests *.py *.designspace glyphslib-2.2.1/MetaTools/000077500000000000000000000000001322341616200154205ustar00rootroot00000000000000glyphslib-2.2.1/MetaTools/generate_glyphdata.py000066400000000000000000000251101322341616200216200ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from fontTools.misc.py23 import * import sys sys.path.append("./Lib") import io import fontTools.agl import json import urllib import textwrap import xml.etree.ElementTree as etree from collections import Counter, defaultdict, namedtuple from glyphsLib.glyphdata import get_glyph, _get_unicode_category, _get_category # Data tables which we put into the generated Python file. # See comments in generate_python_source() below for documentation. GlyphData = namedtuple('GlyphData', [ 'PRODUCTION_NAMES', 'PRODUCTION_NAMES_REVERSED', 'IRREGULAR_UNICODE_STRINGS', 'MISSING_UNICODE_STRINGS', 'DEFAULT_CATEGORIES', 'IRREGULAR_CATEGORIES', ]) def fetch_url(url): try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen stream = urlopen(url) content = stream.read() stream.close() return content.decode('utf-8') def fetch(filename): return fetch_url( "https://raw.githubusercontent.com/schriftgestalt/GlyphsInfo/master/" + filename) def fetch_data_version(): last_commit_info = fetch_url( "https://api.github.com/repos/schriftgestalt/GlyphsInfo/commits/master") return json.loads(last_commit_info)["sha"] def fetch_all_glyphs(): glyphs = {} for filename in ("GlyphData.xml", "GlyphData_Ideographs.xml"): for glyph in etree.fromstring(fetch(filename)).findall("glyph"): glyphName = glyph.attrib["name"] assert glyphName not in glyphs, "multiple entries for " + glyphName glyphs[glyphName] = glyph.attrib return glyphs def load_file(filename): stream = open(filename, "r") content = stream.read() stream.close() return content def load_all_glyphs_from_files(filenames): glyphs = {} for filename in filenames: for glyph in etree.fromstring(load_file(filename)).findall("glyph"): glyphName = glyph.attrib["name"] assert glyphName not in glyphs, "multiple entries for " + glyphName glyphs[glyphName] = glyph.attrib return glyphs def build_data(glyphs): default_categories, irregular_categories = build_categories(glyphs) prodnames = {} irregular_unicode_strings = {} missing_unicode_strings = set() for name, glyph in glyphs.items(): prodname = glyph.get("production", name) if prodname != name: prodnames[name] = prodname inferred_unistr = fontTools.agl.toUnicode(prodname) unistr = glyph.get("unicode") unistr = unichr(int(unistr, 16)) if unistr else None if unistr is None: missing_unicode_strings.add(name) elif unistr != inferred_unistr: irregular_unicode_strings[name] = unistr prodnames_rev = {agl: g for g, agl in prodnames.items()} return GlyphData(prodnames, prodnames_rev, irregular_unicode_strings, missing_unicode_strings, default_categories, irregular_categories) def build_categories(glyphs): counts = defaultdict(Counter) unicode_strings = {} for name, glyph in glyphs.items(): prodname = glyph.get("production", name) unistr = unicode_strings[name] = fontTools.agl.toUnicode(prodname) unicode_category = _get_unicode_category(unistr) category = (glyph.get("category"), glyph.get("subCategory")) counts[unicode_category][category] += 1 default_categories = {"Cc": ("Separator", None)} for key, value in counts.items(): cat, _count = value.most_common(1)[0] default_categories[key] = cat # Find irregular categories. Whether it makes much sense for # Glyphs.app to disagree with Unicode about Unicode categories, # and whether it's a great idea to introduce inconsistencies (for # example, other than Unicode, Glyphs does not assign the same # category to "ampersand" and "ampersand.full"), is an entirely # moot question. Our goal here is to return the same properties as # encoded in GlyphsData.xml, so that glyphsLib produces the same # output as Glyphs.app. # # Changing the category of one glyph can affect the category of # others. To handle this correctly, we execute a simple fixed # point algorithm. Each iteration looks for glyphs whose category # is different from what we'd have inferred from the current data # tables; any irregularities get added to the irregular_categories # exception list. If the last iteration has discovered additional # irregularites, we do another round, trying to expand the exception # list until we cannot find any more. irregular_categories = {} data = GlyphData({}, {}, {}, set(), default_categories, irregular_categories) changed = True while changed: changed = False for name, glyph in glyphs.items(): inferred_category = _get_category(name, unicode_strings[name], data) category = (glyph.get("category"), glyph.get("subCategory")) if category != inferred_category: irregular_categories[name] = category changed = True return default_categories, irregular_categories def test_data(glyphs, data): """Runs checks on the generated GlyphData Makes sure that the implementation of glyphsLib.glyphdata.get_glyph(), if it were to work on the generated GlyphData, will produce the exact same results as the original data files. """ for _, glyph in sorted(glyphs.items()): name = glyph["name"] prod = glyph.get("production", name) unicode = glyph.get("unicode") unicode = unichr(int(unicode, 16)) if unicode else None category = glyph.get("category") subCategory = glyph.get("subCategory") g = get_glyph(name, data=data) assert name == g.name, (name, g.name) assert prod == g.production_name, (name, prod, g.production_name) assert unicode == g.unicode, (name, unicode, g.unicode) assert category == g.category, (name, category, g.category) assert subCategory == g.subCategory, (name, subCategory, g.subCategory) def nonesorter(a): # Python 2 sorts None before any string (even empty string), while # Python 3 raises a TypeError when attempting to compare NoneType with str. # Here we emulate python 2 and return "" when an item to be sorted is None if isinstance(a, tuple): return tuple(nonesorter(e) for e in a) return "" if a is None else a def generate_python_source(data, out): out.write( "# -*- coding: utf-8 -*-\n" "#\n" "# Please do not manually edit this file.\n" "#\n") if len(sys.argv) < 2: out.write( "# It has been generated by MetaTools/generate_glyphdata.py using\n" "# upstream data from https://github.com/schriftgestalt/GlyphsInfo/\n" "# taken at commit hash %s.\n" "#\n" % fetch_data_version()) for paragraph in fetch("LICENSE").strip().split("\n\n"): out.write("#\n") for line in textwrap.wrap(paragraph): out.write("# ") out.write(line) out.write("\n") out.write("\nfrom __future__ import unicode_literals\n\n\n") out.write( "# Glyphs for which Glyphs.app uses production names that do not\n" "# comply with the Adobe Glyph List specification.\n") out.write("PRODUCTION_NAMES = {\n") for key, value in sorted(data.PRODUCTION_NAMES.items()): out.write('\t"%s":"%s",\n' % (key, value)) out.write("}\n\n") out.write("PRODUCTION_NAMES_REVERSED = {\n" "\tagl: g for g, agl in PRODUCTION_NAMES.items()\n" "}\n\n") out.write( "# Glyphs for which Glyphs.app has a different Unicode string\n" "# than the string we would generate from the production name.\n") out.write("IRREGULAR_UNICODE_STRINGS = {\n") for key, value in sorted(data.IRREGULAR_UNICODE_STRINGS.items()): value_repr = value.encode("unicode-escape").decode('ascii') out.write('\t"%s":"%s",\n' % (key, value_repr)) out.write("}\n\n") out.write( "# Glyphs for which Glyphs.app has no Unicode string.\n" "# For almost all these glyphs, one could derive a Unicode string\n" "# from the production glyph name, but Glyphs.app still has none\n" "# in its data. Many of these cases seem to be bugs in GlyphsData,\n" "# but we need to be compatible with Glyphs.\n") out.write("MISSING_UNICODE_STRINGS = {\n") for name in sorted(data.MISSING_UNICODE_STRINGS): out.write('\t"%s",\n' % name) out.write("}\n\n") out.write( "# From the first character of the Unicode string of a glyph,\n" "# one can compute the Unicode category. This Unicode category\n" "# can frequently be mapped to the Glyphs category and subCategory.\n" "DEFAULT_CATEGORIES = {\n") for ucat, glyphsCat in sorted( data.DEFAULT_CATEGORIES.items(), key=nonesorter): out.write('\t%s: %s,\n' % ('"%s"' % ucat if ucat else 'None', glyphsCat)) out.write("}\n\n") out.write( "# However, to some glyphs, Glyphs.app assigns a different category\n" "# or sub-category than Unicode. The following table contains these\n" "# exceptions.\n" "IRREGULAR_CATEGORIES = {\n") for glyphName, glyphsCat in sorted( data.IRREGULAR_CATEGORIES.items(), key=nonesorter): out.write('\t"%s": %s,\n' % (glyphName, glyphsCat)) out.write("}\n\n") if __name__ == "__main__": outpath = "Lib/glyphsLib/glyphdata_generated.py" glyphs = ( load_all_glyphs_from_files(sys.argv[1:]) if len(sys.argv) >= 2 else fetch_all_glyphs()) data = build_data(glyphs) test_data(glyphs, data) with io.open(outpath, "w", encoding="utf-8") as out: generate_python_source(data, out) glyphslib-2.2.1/README.rst000066400000000000000000000036631322341616200152100ustar00rootroot00000000000000|Travis Build Status| |PyPI Version| |Codecov| glyphsLib ========= This library provides a bridge from Glyphs source files (.glyphs) to UFOs via `defcon `__. The main methods for conversion are found in ``__init__.py``. Intermediate data can be accessed without actually writing UFOs, if needed. Write and return UFOs ^^^^^^^^^^^^^^^^^^^^^ Masters: .. code:: python master_dir = 'master_ufos' ufos = glyphsLib.build_masters('MyFont.glyphs', master_dir) Interpolated instances (depends on `MutatorMath `__): .. code:: python master_dir = 'master_ufos' instance_dir = 'instance_ufos' ufos = glyphsLib.build_instances('MyFont.glyphs', master_dir, instance_dir) Load UFO objects without writing ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: python ufos = glyphsLib.load_to_ufos('MyFont.glyphs') Read and write Glyphs data as Python objects ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: python from glyphsLib import GSFont font = GSFont(glyphs_file) font.save(glyphs_file) The ``glyphsLib.classes`` module aims to provide an interface similar to Glyphs.app's `Python Scripting API `__. Note that currently not all the classes and methods may be fully implemented. We try to keep up to date, but if you find something that is missing or does not work as expected, please open a issue. .. TODO Briefly state how much of the Glyphs.app API is currently covered, and what is not supported yet. .. |Travis Build Status| image:: https://travis-ci.org/googlei18n/glyphsLib.svg :target: https://travis-ci.org/googlei18n/glyphsLib .. |PyPI Version| image:: https://img.shields.io/pypi/v/glyphsLib.svg :target: https://pypi.org/project/glyphsLib/ .. |Codecov| image:: https://codecov.io/gh/googlei18n/glyphsLib/branch/master/graph/badge.svg :target: https://codecov.io/gh/googlei18n/glyphsLib glyphslib-2.2.1/requirements.txt000066400000000000000000000000631322341616200167740ustar00rootroot00000000000000fonttools==3.19.0 defcon==0.3.5 MutatorMath==2.1.0 glyphslib-2.2.1/setup.cfg000066400000000000000000000014611322341616200153340ustar00rootroot00000000000000[bumpversion] current_version = 2.2.1 commit = True tag = False tag_name = v{new_version} parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? serialize = {major}.{minor}.{patch}.{release}{dev} {major}.{minor}.{patch} [bumpversion:part:release] optional_value = final values = dev final [bumpversion:part:dev] [bumpversion:file:Lib/glyphsLib/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" [bumpversion:file:setup.py] search = version='{current_version}' replace = version='{new_version}' [wheel] universal = 1 [sdist] formats = zip [aliases] test = pytest [metadata] license_file = LICENSE [tool:pytest] minversion = 2.8 testpaths = tests python_files = *_test.py python_classes = *Test addopts = -v -r a glyphslib-2.2.1/setup.py000066400000000000000000000156441322341616200152350ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from setuptools import setup, find_packages, Command from distutils import log class bump_version(Command): description = "increment the package version and commit the changes" user_options = [ ("major", None, "bump the first digit, for incompatible API changes"), ("minor", None, "bump the second digit, for new backward-compatible features"), ("patch", None, "bump the third digit, for bug fixes (default)"), ] def initialize_options(self): self.minor = False self.major = False self.patch = False def finalize_options(self): part = None for attr in ("major", "minor", "patch"): if getattr(self, attr, False): if part is None: part = attr else: from distutils.errors import DistutilsOptionError raise DistutilsOptionError( "version part options are mutually exclusive") self.part = part or "patch" def bumpversion(self, part, **kwargs): """ Run bumpversion.main() with the specified arguments. """ import bumpversion args = ['--verbose'] if self.verbose > 1 else [] for k, v in kwargs.items(): k = "--{}".format(k.replace("_", "-")) is_bool = isinstance(v, bool) and v is True args.extend([k] if is_bool else [k, str(v)]) args.append(part) log.debug( "$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args)) bumpversion.main(args) def run(self): log.info("bumping '%s' version" % self.part) self.bumpversion(self.part) class release(bump_version): """Drop the developmental release '.devN' suffix from the package version, open the default text $EDITOR to write release notes, commit the changes and generate a git tag. Release notes can also be set with the -m/--message option, or by reading from standard input. """ description = "tag a new release" user_options = [ ("message=", 'm', "message containing the release notes"), ] def initialize_options(self): self.message = None def finalize_options(self): import re current_version = self.distribution.metadata.get_version() if not re.search(r"\.dev[0-9]+", current_version): from distutils.errors import DistutilsSetupError raise DistutilsSetupError( "current version (%s) has no '.devN' suffix.\n " "Run 'setup.py bump_version' with any of " "--major, --minor, --patch options" % current_version) message = self.message if message is None: if sys.stdin.isatty(): # stdin is interactive, use editor to write release notes message = self.edit_release_notes() else: # read release notes from stdin pipe message = sys.stdin.read() if not message.strip(): from distutils.errors import DistutilsSetupError raise DistutilsSetupError("release notes message is empty") self.message = "v{new_version}\n\n%s" % message @staticmethod def edit_release_notes(): """Use the default text $EDITOR to write release notes. If $EDITOR is not set, use 'nano'.""" from tempfile import mkstemp import os import shlex import subprocess text_editor = shlex.split(os.environ.get('EDITOR', 'nano')) fd, tmp = mkstemp(prefix='bumpversion-') try: os.close(fd) with open(tmp, 'w') as f: f.write("\n\n# Write release notes.\n" "# Lines starting with '#' will be ignored.") subprocess.check_call(text_editor + [tmp]) with open(tmp, 'r') as f: changes = "".join( l for l in f.readlines() if not l.startswith('#')) finally: os.remove(tmp) return changes def run(self): log.info("stripping developmental release suffix") # drop '.dev0' suffix, commit with given message and create git tag self.bumpversion("release", tag=True, message="Release {new_version}", tag_message=self.message) needs_pytest = {'pytest', 'test'}.intersection(sys.argv) pytest_runner = ['pytest_runner'] if needs_pytest else [] needs_wheel = {'bdist_wheel'}.intersection(sys.argv) wheel = ['wheel'] if needs_wheel else [] needs_bump2version = {'release', 'bump_version'}.intersection(sys.argv) bump2version = ['bump2version'] if needs_bump2version else [] with open('README.rst', 'r') as f: long_description = f.read() test_requires = [ 'pytest>=2.8', ] if sys.version_info < (3, 3): test_requires.append('mock>=2.0.0') setup( name='glyphsLib', version='2.2.1', author="James Godfrey-Kittle", author_email="jamesgk@google.com", description="A bridge from Glyphs source files (.glyphs) to UFOs", long_description=long_description, url="https://github.com/googlei18n/glyphsLib", license="Apache Software License 2.0", package_dir={"": "Lib"}, packages=find_packages("Lib"), entry_points={ "console_scripts": [ "glyphs2ufo = glyphsLib.__main__:main" ], }, setup_requires=pytest_runner + wheel + bump2version, tests_require=test_requires, install_requires=[ "fonttools>=3.4.0", "defcon>=0.3.0", "MutatorMath>=2.0.4", ], cmdclass={ "release": release, "bump_version": bump_version, }, classifiers=[ 'Development Status :: 4 - Beta', "Environment :: Console", "Environment :: Other Environment", 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Multimedia :: Graphics', 'Topic :: Multimedia :: Graphics :: Graphics Conversion', 'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based', ], ) glyphslib-2.2.1/tests/000077500000000000000000000000001322341616200146535ustar00rootroot00000000000000glyphslib-2.2.1/tests/builder_test.py000066400000000000000000001330751322341616200177230ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) import collections import datetime from textwrap import dedent import io import logging import unittest # unittest.mock is only available for python 3.3+ try: from unittest import mock from unittest.mock import patch except ImportError: from mock import patch import mock from defcon import Font from fontTools.misc.loggingTools import CapturingLogHandler from glyphsLib import builder from glyphsLib.classes import ( GSFont, GSFontMaster, GSInstance, GSCustomParameter, GSGlyph, GSLayer, GSPath, GSNode, GSAnchor, GSComponent, GSAlignmentZone, GSGuideLine) from glyphsLib.types import point from glyphsLib.builder import to_ufos from glyphsLib.builder.paths import to_ufo_draw_paths from glyphsLib.builder.custom_params import (set_custom_params, set_default_params) from glyphsLib.builder.names import build_stylemap_names, build_style_name from glyphsLib.builder.filters import parse_glyphs_filter from glyphsLib.builder.constants import ( GLYPHS_PREFIX, PUBLIC_PREFIX, GLYPHLIB_PREFIX, UFO2FT_USE_PROD_NAMES_KEY) from classes_test import (generate_minimal_font, generate_instance_from_dict, add_glyph, add_anchor, add_component) class BuildStyleNameTest(unittest.TestCase): def test_style_regular_weight(self): self.assertEqual(build_style_name(is_italic=False), 'Regular') self.assertEqual(build_style_name(is_italic=True), 'Italic') def test_style_nonregular_weight(self): self.assertEqual( build_style_name(weight='Thin', is_italic=False), 'Thin') self.assertEqual( build_style_name(weight='Thin', is_italic=True), 'Thin Italic') def test_style_nonregular_width(self): self.assertEqual( build_style_name(width='Condensed', is_italic=False), 'Condensed') self.assertEqual( build_style_name(width='Condensed', is_italic=True), 'Condensed Italic') self.assertEqual( build_style_name(weight='Thin', width='Condensed', is_italic=False), 'Condensed Thin') self.assertEqual( build_style_name(weight='Thin', width='Condensed', is_italic=True), 'Condensed Thin Italic') def test_style_custom(self): self.assertEqual( build_style_name(custom='Text', is_italic=False), 'Text') self.assertEqual( build_style_name(weight='Text', is_italic=True), 'Text Italic') class BuildStyleMapNamesTest(unittest.TestCase): def test_regular(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Regular", is_bold=False, is_italic=False, linked_style=None ) self.assertEqual("NotoSans", map_family) self.assertEqual("regular", map_style) def test_regular_isBold(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Regular", is_bold=True, is_italic=False, linked_style=None ) self.assertEqual("NotoSans Regular", map_family) self.assertEqual("bold", map_style) def test_regular_isItalic(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Regular", is_bold=False, is_italic=True, linked_style=None ) self.assertEqual("NotoSans Regular", map_family) self.assertEqual("italic", map_style) def test_non_regular(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="ExtraBold", is_bold=False, is_italic=False, linked_style=None ) self.assertEqual("NotoSans ExtraBold", map_family) self.assertEqual("regular", map_style) def test_bold_no_style_link(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Bold", is_bold=False, # not style-linked, despite the name is_italic=False, linked_style=None ) self.assertEqual("NotoSans Bold", map_family) self.assertEqual("regular", map_style) def test_italic_no_style_link(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Italic", is_bold=False, is_italic=False, # not style-linked, despite the name linked_style=None ) self.assertEqual("NotoSans Italic", map_family) self.assertEqual("regular", map_style) def test_bold_italic_no_style_link(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Bold Italic", is_bold=False, # not style-linked, despite the name is_italic=False, # not style-linked, despite the name linked_style=None ) self.assertEqual("NotoSans Bold Italic", map_family) self.assertEqual("regular", map_style) def test_bold(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Bold", is_bold=True, is_italic=False, linked_style=None ) self.assertEqual("NotoSans", map_family) self.assertEqual("bold", map_style) def test_italic(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Italic", is_bold=False, is_italic=True, linked_style=None ) self.assertEqual("NotoSans", map_family) self.assertEqual("italic", map_style) def test_bold_italic(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Bold Italic", is_bold=True, is_italic=True, linked_style=None ) self.assertEqual("NotoSans", map_family) self.assertEqual("bold italic", map_style) def test_incomplete_bold_italic(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Bold", # will be stripped... is_bold=True, is_italic=True, linked_style=None ) self.assertEqual("NotoSans", map_family) self.assertEqual("bold italic", map_style) map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Italic", # will be stripped... is_bold=True, is_italic=True, linked_style=None ) self.assertEqual("NotoSans", map_family) self.assertEqual("bold italic", map_style) def test_italicbold_isBoldItalic(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Italic Bold", # reversed is_bold=True, is_italic=True, linked_style=None ) self.assertEqual("NotoSans", map_family) self.assertEqual("bold italic", map_style) def test_linked_style_regular(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Condensed", is_bold=False, is_italic=False, linked_style="Cd" ) self.assertEqual("NotoSans Cd", map_family) self.assertEqual("regular", map_style) def test_linked_style_bold(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Condensed Bold", is_bold=True, is_italic=False, linked_style="Cd" ) self.assertEqual("NotoSans Cd", map_family) self.assertEqual("bold", map_style) def test_linked_style_italic(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Condensed Italic", is_bold=False, is_italic=True, linked_style="Cd" ) self.assertEqual("NotoSans Cd", map_family) self.assertEqual("italic", map_style) def test_linked_style_bold_italic(self): map_family, map_style = build_stylemap_names( family_name="NotoSans", style_name="Condensed Bold Italic", is_bold=True, is_italic=True, linked_style="Cd" ) self.assertEqual("NotoSans Cd", map_family) self.assertEqual("bold italic", map_style) class SetCustomParamsTest(unittest.TestCase): def setUp(self): self.ufo = Font() def test_normalizes_curved_quotes_in_names(self): master = GSFontMaster() master.customParameters = [GSCustomParameter(name='‘bad’', value=1), GSCustomParameter(name='“also bad”', value=2)] set_custom_params(self.ufo, data=master) self.assertIn(GLYPHS_PREFIX + "'bad'", self.ufo.lib) self.assertIn(GLYPHS_PREFIX + '"also bad"', self.ufo.lib) def test_set_glyphOrder(self): set_custom_params(self.ufo, parsed=[('glyphOrder', ['A', 'B'])]) self.assertEqual(self.ufo.lib[PUBLIC_PREFIX + 'glyphOrder'], ['A', 'B']) def test_set_fsSelection_flags(self): self.assertEqual(self.ufo.info.openTypeOS2Selection, None) set_custom_params(self.ufo, parsed=[('Has WWS Names', False)]) self.assertEqual(self.ufo.info.openTypeOS2Selection, None) set_custom_params(self.ufo, parsed=[('Use Typo Metrics', True)]) self.assertEqual(self.ufo.info.openTypeOS2Selection, [7]) self.ufo = Font() set_custom_params(self.ufo, parsed=[('Has WWS Names', True), ('Use Typo Metrics', True)]) self.assertEqual(self.ufo.info.openTypeOS2Selection, [8, 7]) def test_underlinePosition(self): set_custom_params(self.ufo, parsed=[('underlinePosition', -2)]) self.assertEqual(self.ufo.info.postscriptUnderlinePosition, -2) set_custom_params(self.ufo, parsed=[('underlinePosition', 1)]) self.assertEqual(self.ufo.info.postscriptUnderlinePosition, 1) def test_underlineThickness(self): set_custom_params(self.ufo, parsed=[('underlineThickness', 100)]) self.assertEqual(self.ufo.info.postscriptUnderlineThickness, 100) set_custom_params(self.ufo, parsed=[('underlineThickness', 0)]) self.assertEqual(self.ufo.info.postscriptUnderlineThickness, 0) @patch('glyphsLib.builder.custom_params.parse_glyphs_filter') def test_parse_glyphs_filter(self, mock_parse_glyphs_filter): filter1 = ('PreFilter', 'AddExtremes') filter2 = ( 'Filter', 'Transformations;OffsetX:40;OffsetY:60;include:uni0334,uni0335') filter3 = ( 'Filter', 'Transformations;OffsetX:10;OffsetY:-10;exclude:uni0334,uni0335') set_custom_params(self.ufo, parsed=[filter1, filter2, filter3]) self.assertEqual(mock_parse_glyphs_filter.call_count, 3) self.assertEqual(mock_parse_glyphs_filter.call_args_list[0], mock.call(filter1[1], is_pre=True)) self.assertEqual(mock_parse_glyphs_filter.call_args_list[1], mock.call(filter2[1], is_pre=False)) self.assertEqual(mock_parse_glyphs_filter.call_args_list[2], mock.call(filter3[1], is_pre=False)) def test_set_defaults(self): set_default_params(self.ufo) self.assertEqual(self.ufo.info.openTypeOS2Type, [3]) self.assertEqual(self.ufo.info.postscriptUnderlinePosition, -100) self.assertEqual(self.ufo.info.postscriptUnderlineThickness, 50) def test_set_codePageRanges(self): set_custom_params(self.ufo, parsed=[('codePageRanges', [1252, 1250])]) self.assertEqual(self.ufo.info.openTypeOS2CodePageRanges, [0, 1]) def test_set_openTypeOS2CodePageRanges(self): set_custom_params(self.ufo, parsed=[ ('openTypeOS2CodePageRanges', [0, 1])]) self.assertEqual(self.ufo.info.openTypeOS2CodePageRanges, [0, 1]) def test_gasp_table(self): gasp_table = {'65535': '15', '20': '7', '8': '10'} set_custom_params(self.ufo, parsed=[('GASP Table', gasp_table)]) ufo_range_records = self.ufo.info.openTypeGaspRangeRecords self.assertIsNotNone(ufo_range_records) self.assertEqual(len(ufo_range_records), 3) rec1, rec2, rec3 = ufo_range_records self.assertEqual(rec1['rangeMaxPPEM'], 8) self.assertEqual(rec1['rangeGaspBehavior'], [1, 3]) self.assertEqual(rec2['rangeMaxPPEM'], 20) self.assertEqual(rec2['rangeGaspBehavior'], [0, 1, 2]) self.assertEqual(rec3['rangeMaxPPEM'], 65535) self.assertEqual(rec3['rangeGaspBehavior'], [0, 1, 2, 3]) def test_set_disables_nice_names(self): set_custom_params(self.ufo, parsed=[('disablesNiceNames', False)]) self.assertEqual(True, self.ufo.lib[GLYPHS_PREFIX + 'useNiceNames']) def test_set_disable_last_change(self): set_custom_params(self.ufo, parsed=[('Disable Last Change', True)]) self.assertEqual(True, self.ufo.lib[GLYPHS_PREFIX + 'disablesLastChange']) def test_xHeight(self): set_custom_params(self.ufo, parsed=[('xHeight', '500')]) self.assertEqual(self.ufo.info.xHeight, 500) def test_replace_feature(self): self.ufo.features.text = dedent(""" feature liga { # only the first match is replaced sub f i by fi; } liga; feature calt { sub e' t' c by ampersand; } calt; feature liga { sub f l by fl; } liga; """) repl = "liga; sub f f by ff;" set_custom_params(self.ufo, parsed=[("Replace Feature", repl)]) self.assertEqual(self.ufo.features.text, dedent(""" feature liga { sub f f by ff; } liga; feature calt { sub e' t' c by ampersand; } calt; feature liga { sub f l by fl; } liga; """)) # only replace feature body if tag already present original = self.ufo.features.text repl = "numr; sub one by one.numr;\nsub two by two.numr;\n" set_custom_params(self.ufo, parsed=[("Replace Feature", repl)]) self.assertEqual(self.ufo.features.text, original) def test_useProductionNames(self): for value in (True, False): glyphs_param = ("Don't use Production Names", value) set_custom_params(self.ufo, parsed=[glyphs_param]) self.assertIn(UFO2FT_USE_PROD_NAMES_KEY, self.ufo.lib) self.assertEqual(self.ufo.lib[UFO2FT_USE_PROD_NAMES_KEY], not value) class ParseGlyphsFilterTest(unittest.TestCase): def test_complete_parameter(self): inputstr = 'Transformations;LSB:+23;RSB:-22;SlantCorrection:true;OffsetX:10;OffsetY:-10;Origin:0;exclude:uni0334,uni0335 uni0336' expected = { 'name': 'Transformations', 'kwargs': { 'LSB': 23, 'RSB': -22, 'SlantCorrection': True, 'OffsetX': 10, 'OffsetY': -10, 'Origin': 0, }, 'exclude': ['uni0334', 'uni0335', 'uni0336'], } result = parse_glyphs_filter(inputstr) self.assertEqual(result, expected) def test_is_pre(self): inputstr = 'Dummy' expected = { 'name': 'Dummy', 'pre': True, } result = parse_glyphs_filter(inputstr, is_pre=True) self.assertEqual(result, expected) def test_positional_parameter(self): inputstr = 'Roughenizer;34;2;0;0.34' expected = { 'name': 'Roughenizer', 'args': [34, 2, 0, 0.34], } result = parse_glyphs_filter(inputstr) self.assertEqual(result, expected) def test_single_name(self): inputstr = 'AddExtremes' expected = { 'name': 'AddExtremes', } result = parse_glyphs_filter(inputstr) self.assertEqual(result, expected) def test_empty_string(self): inputstr = '' with CapturingLogHandler(builder.logger, "ERROR") as captor: result = parse_glyphs_filter(inputstr) self.assertGreater(len([r for r in captor.records if 'Failed to parse glyphs filter' in r.msg]), 0, msg='Empty string should trigger an error message') def test_no_name(self): inputstr = ';OffsetX:2' with CapturingLogHandler(builder.logger, "ERROR") as captor: result = parse_glyphs_filter(inputstr) self.assertGreater(len([r for r in captor.records if 'Failed to parse glyphs filter' in r.msg]), 0, msg='Empty string with no filter name should trigger an error message') def test_duplicate_exclude_include(self): inputstr = 'thisisaname;34;-3.4;exclude:uni1111;include:uni0022;exclude:uni2222' expected = { 'name': 'thisisaname', 'args': [34, -3.4], 'exclude': ['uni2222'], } with CapturingLogHandler(builder.logger, "ERROR") as captor: result = parse_glyphs_filter(inputstr) self.assertGreater(len([r for r in captor.records if 'can only present as the last argument' in r.msg]), 0, msg='The parse_glyphs_filter should warn user that the exclude/include should only be the last argument in the filter string.') self.assertEqual(result, expected) def test_empty_args_trailing_semicolon(self): inputstr = 'thisisaname;3;;a:b;;;' expected = { 'name': 'thisisaname', 'args': [3], 'kwargs': {'a': 'b'} } result = parse_glyphs_filter(inputstr) self.assertEqual(result, expected) class ToUfosTest(unittest.TestCase): def test_minimal_data(self): """Test the minimal data that must be provided to generate UFOs, and in some cases that additional redundant data is not set. """ font = generate_minimal_font() family_name = font.familyName ufos = to_ufos(font) self.assertEqual(len(ufos), 1) ufo = ufos[0] self.assertEqual(len(ufo), 0) self.assertEqual(ufo.info.familyName, family_name) # self.assertEqual(ufo.info.styleName, 'Regular') self.assertEqual(ufo.info.versionMajor, 1) self.assertEqual(ufo.info.versionMinor, 0) self.assertIsNone(ufo.info.openTypeNameVersion) #TODO(jamesgk) try to generate minimally-populated UFOs in glyphsLib, # assert that more fields are empty here (especially in name table) def test_warn_no_version(self): """Test that a warning is printed when app version is missing.""" font = generate_minimal_font() font.appVersion = "0" with CapturingLogHandler(builder.logger, "WARNING") as captor: to_ufos(font) self.assertEqual(len([r for r in captor.records if "outdated version" in r.msg]), 1) def test_load_kerning(self): """Test that kerning conflicts are resolved correctly. Correct resolution is defined as such: the last time a pair is found in a kerning rule, that rule is used for the pair. """ font = generate_minimal_font() # generate classes 'A': ['A', 'a'] and 'V': ['V', 'v'] for glyph_name in ('A', 'a', 'V', 'v'): glyph = add_glyph(font, glyph_name) glyph.rightKerningGroup = glyph_name.upper() glyph.leftKerningGroup = glyph_name.upper() # classes are referenced in Glyphs kerning using old MMK names font.kerning = { font.masters[0].id: collections.OrderedDict(( ('@MMK_L_A', collections.OrderedDict(( ('@MMK_R_V', -250), ('v', -100), ))), ('a', collections.OrderedDict(( ('@MMK_R_V', 100), ))), ))} ufos = to_ufos(font) ufo = ufos[0] # these rules should be obvious self.assertEqual(ufo.kerning['public.kern1.A', 'public.kern2.V'], -250) self.assertEqual(ufo.kerning['a', 'public.kern2.V'], 100) # this rule results from breaking up (kern1.A, v, -100) # due to conflict with (a, kern2.V, 100) self.assertEqual(ufo.kerning['A', 'v'], -100) def test_propagate_anchors(self): """Test anchor propagation for some relatively complicated cases.""" font = generate_minimal_font() glyphs = ( ('sad', [], [('bottom', 50, -50), ('top', 50, 150)]), ('dotabove', [], [('top', 0, 150), ('_top', 0, 100)]), ('dotbelow', [], [('bottom', 0, -50), ('_bottom', 0, 0)]), ('dad', [('sad', 0, 0), ('dotabove', 50, 50)], []), ('dadDotbelow', [('dad', 0, 0), ('dotbelow', 50, -50)], []), ('yod', [], [('bottom', 50, -50)]), ('yodyod', [('yod', 0, 0), ('yod', 100, 0)], []), ) for name, component_data, anchor_data in glyphs: glyph = add_glyph(font, name) for n, x, y, in anchor_data: add_anchor(font, name, n, x, y) for n, x, y in component_data: add_component(font, name, n, (1, 0, 0, 1, x, y)) ufos = to_ufos(font) ufo = ufos[0] glyph = ufo['dadDotbelow'] self.assertEqual(len(glyph.anchors), 2) # check propagated anchors are appended in a deterministic order self.assertEqual( [anchor.name for anchor in glyph.anchors], ['bottom', 'top'] ) for anchor in glyph.anchors: self.assertEqual(anchor.x, 50) if anchor.name == 'bottom': self.assertEqual(anchor.y, -100) else: self.assertEqual(anchor.name, 'top') self.assertEqual(anchor.y, 200) glyph = ufo['yodyod'] self.assertEqual(len(glyph.anchors), 2) for anchor in glyph.anchors: self.assertEqual(anchor.y, -50) if anchor.name == 'bottom_1': self.assertEqual(anchor.x, 50) else: self.assertEqual(anchor.name, 'bottom_2') self.assertEqual(anchor.x, 150) def test_postscript_name_from_data(self): font = generate_minimal_font() add_glyph(font, 'foo')['production'] = 'f_o_o.alt1' ufo = to_ufos(font)[0] postscriptNames = ufo.lib.get('public.postscriptNames') self.assertEqual(postscriptNames, {'foo': 'f_o_o.alt1'}) def test_postscript_name_from_glyph_name(self): font = generate_minimal_font() # in GlyphData (and AGLFN) without a 'production' name add_glyph(font, 'A') # not in GlyphData, no production name add_glyph(font, 'foobar') # in GlyphData with a 'production' name add_glyph(font, 'C-fraktur') ufo = to_ufos(font)[0] postscriptNames = ufo.lib.get('public.postscriptNames') self.assertEqual(postscriptNames, {'C-fraktur': 'uni212D'}) def test_category(self): font = generate_minimal_font() add_glyph(font, 'foo')['category'] = 'Mark' add_glyph(font, 'bar') ufo = to_ufos(font)[0] category_key = GLYPHLIB_PREFIX + 'category' self.assertEqual(ufo['foo'].lib.get(category_key), 'Mark') self.assertFalse(category_key in ufo['bar'].lib) def test_subCategory(self): font = generate_minimal_font() add_glyph(font, 'foo')['subCategory'] = 'Nonspacing' add_glyph(font, 'bar') ufo = to_ufos(font)[0] subCategory_key = GLYPHLIB_PREFIX + 'subCategory' self.assertEqual(ufo['foo'].lib.get(subCategory_key), 'Nonspacing') self.assertFalse(subCategory_key in ufo['bar'].lib) def test_mark_nonspacing_zero_width(self): font = generate_minimal_font() add_glyph(font, 'dieresiscomb').layers[0].width = 100 foo = add_glyph(font, 'foo') foo.category = 'Mark' foo.subCategory = 'Nonspacing' foo.layers[0].width = 200 bar = add_glyph(font, 'bar') bar.category = 'Mark' bar.subCategory = 'Nonspacing' bar.layers[0].width = 0 ufo = to_ufos(font)[0] originalWidth_key = GLYPHLIB_PREFIX + 'originalWidth' self.assertEqual(ufo['dieresiscomb'].width, 0) self.assertEqual(ufo['dieresiscomb'].lib.get(originalWidth_key), 100) self.assertEqual(ufo['foo'].width, 0) self.assertEqual(ufo['foo'].lib.get(originalWidth_key), 200) self.assertEqual(ufo['bar'].width, 0) self.assertFalse(originalWidth_key in ufo['bar'].lib) def test_GDEF(self): font = generate_minimal_font() for glyph in ('space', 'A', 'A.alt', 'wigglylinebelowcomb', 'wigglylinebelowcomb.alt', 'fi', 'fi.alt', 't_e_s_t', 't_e_s_t.alt'): add_glyph(font, glyph) add_anchor(font, 'A', 'bottom', 300, -10) add_anchor(font, 'wigglylinebelowcomb', '_bottom', 100, 40) add_anchor(font, 'fi', 'caret_1', 150, 0) add_anchor(font, 't_e_s_t.alt', 'caret_1', 200, 0) add_anchor(font, 't_e_s_t.alt', 'caret_2', 400, 0) add_anchor(font, 't_e_s_t.alt', 'caret_3', 600, 0) ufo = to_ufos(font)[0] self.assertEqual(ufo.features.text.splitlines(), [ 'table GDEF {', ' # automatic', ' GlyphClassDef', ' [A], # Base', ' [fi t_e_s_t.alt], # Liga', ' [wigglylinebelowcomb wigglylinebelowcomb.alt], # Mark', ' ;', ' LigatureCaretByPos fi 150;', ' LigatureCaretByPos t_e_s_t.alt 200 400 600;', '} GDEF;', ]) def test_GDEF_base_with_attaching_anchor(self): font = generate_minimal_font() add_glyph(font, 'A.alt') add_anchor(font, 'A.alt', 'top', 400, 1000) self.assertIn('[A.alt], # Base', to_ufos(font)[0].features.text) def test_GDEF_base_with_nonattaching_anchor(self): font = generate_minimal_font() add_glyph(font, 'A.alt') add_anchor(font, 'A.alt', '_top', 400, 1000) self.assertEqual('', to_ufos(font)[0].features.text) def test_GDEF_ligature_with_attaching_anchor(self): font = generate_minimal_font() add_glyph(font, 'fi') add_anchor(font, 'fi', 'top', 400, 1000) self.assertIn('[fi], # Liga', to_ufos(font)[0].features.text) def test_GDEF_ligature_with_nonattaching_anchor(self): font = generate_minimal_font() add_glyph(font, 'fi') add_anchor(font, 'fi', '_top', 400, 1000) self.assertEqual('', to_ufos(font)[0].features.text) def test_GDEF_mark(self): font = generate_minimal_font() add_glyph(font, 'eeMatra-gurmukhi') self.assertIn('[eeMatra-gurmukhi], # Mark', to_ufos(font)[0].features.text) def test_GDEF_fractional_caret_position(self): # Some Glyphs sources happen to contain fractional caret positions. # In the Adobe feature file syntax (and binary OpenType GDEF tables), # caret positions must be integers. font = generate_minimal_font() add_glyph(font, 'fi') add_anchor(font, 'fi', 'caret_1', 499.9876, 0) self.assertIn('LigatureCaretByPos fi 500;', to_ufos(font)[0].features.text) def test_GDEF_custom_category_subCategory(self): font = generate_minimal_font() add_glyph(font, 'foo')['subCategory'] = 'Ligature' add_anchor(font, 'foo', 'top', 400, 1000) bar = add_glyph(font, 'bar') bar['category'], bar['subCategory'] = 'Mark', 'Nonspacing' baz = add_glyph(font, 'baz') baz['category'], baz['subCategory'] = 'Mark', 'Spacing Combining' features = to_ufos(font)[0].features.text self.assertIn('[foo], # Liga', features) self.assertIn('[bar baz], # Mark', features) def test_set_blue_values(self): """Test that blue values are set correctly from alignment zones.""" data_in = [GSAlignmentZone(pos=500, size=15), GSAlignmentZone(pos=400, size=-15), GSAlignmentZone(pos=0, size=-15), GSAlignmentZone(pos=-200, size=15), GSAlignmentZone(pos=-300, size=-15)] expected_blue_values = [-200, -185, -15, 0, 500, 515] expected_other_blues = [-315, -300, 385, 400] font = generate_minimal_font() font.masters[0].alignmentZones = data_in ufo = to_ufos(font)[0] self.assertEqual(ufo.info.postscriptBlueValues, expected_blue_values) self.assertEqual(ufo.info.postscriptOtherBlues, expected_other_blues) def test_set_glyphOrder_no_custom_param(self): font = generate_minimal_font() add_glyph(font, 'C') add_glyph(font, 'B') add_glyph(font, 'A') add_glyph(font, 'Z') glyphOrder = to_ufos(font)[0].lib[PUBLIC_PREFIX + 'glyphOrder'] self.assertEqual(glyphOrder, ['C', 'B', 'A', 'Z']) def test_set_glyphOrder_with_custom_param(self): font = generate_minimal_font() font.customParameters['glyphOrder'] = ['A', 'B', 'C'] add_glyph(font, 'C') add_glyph(font, 'B') add_glyph(font, 'A') # glyphs outside glyphOrder are appended at the end add_glyph(font, 'Z') glyphOrder = to_ufos(font)[0].lib[PUBLIC_PREFIX + 'glyphOrder'] self.assertEqual(glyphOrder, ['A', 'B', 'C', 'Z']) def test_missing_date(self): font = generate_minimal_font() font.date = None ufo = to_ufos(font)[0] self.assertIsNone(ufo.info.openTypeHeadCreated) def test_variation_font_origin(self): font = generate_minimal_font() name = 'Variation Font Origin' value = 'Light' font.customParameters[name] = value ufos, instances = to_ufos(font, include_instances=True) for ufo in ufos: key = GLYPHS_PREFIX + name self.assertIn(key, ufo.lib) self.assertEqual(ufo.lib[key], value) self.assertIn(name, instances) self.assertEqual(instances[name], value) def test_family_name_none(self): font = generate_minimal_font() instances_list = [ { 'name': 'Regular1' }, { 'name': 'Regular2', 'customParameters': [ { 'name': 'familyName', 'value': 'CustomFamily' }, ] } ] font.instances = [generate_instance_from_dict(i) for i in instances_list] # 'family_name' defaults to None ufos, instance_data = to_ufos(font, include_instances=True) instances = instance_data['data'] # all instances are included, both with/without 'familyName' parameter self.assertEqual(len(instances), 2) self.assertEqual(instances[0].name, 'Regular1') self.assertEqual(instances[1].name, 'Regular2') self.assertEqual(len(instances[0].customParameters), 0) self.assertEqual(len(instances[1].customParameters), 1) self.assertEqual(instances[1].customParameters[0].value, 'CustomFamily') # the masters' family name is unchanged for ufo in ufos: self.assertEqual(ufo.info.familyName, 'MyFont') def test_family_name_same_as_default(self): font = generate_minimal_font() instances_list = [ { 'name': 'Regular1' }, { 'name': 'Regular2', 'customParameters': [ { 'name': 'familyName', 'value': 'CustomFamily' }, ] } ] font.instances = [generate_instance_from_dict(i) for i in instances_list] # 'MyFont' is the source family name, as returned from # 'generate_minimal_data' ufos, instance_data = to_ufos(font, include_instances=True, family_name='MyFont') instances = instance_data['data'] # only instances which don't have 'familyName' custom parameter # are included in returned list self.assertEqual(len(instances), 1) self.assertEqual(instances[0].name, 'Regular1') self.assertEqual(len(instances[0].customParameters), 0) # the masters' family name is unchanged for ufo in ufos: self.assertEqual(ufo.info.familyName, 'MyFont') def test_family_name_custom(self): font = generate_minimal_font() instances_list = [ { 'name': 'Regular1' }, { 'name': 'Regular2', 'customParameters': [ { 'name': 'familyName', 'value': 'CustomFamily' }, ] } ] font.instances = [generate_instance_from_dict(i) for i in instances_list] ufos, instance_data = to_ufos(font, include_instances=True, family_name='CustomFamily') instances = instance_data['data'] # only instances with familyName='CustomFamily' are included self.assertEqual(len(instances), 1) self.assertEqual(instances[0].name, 'Regular2') self.assertEqual(len(instances[0].customParameters), 1) self.assertEqual(instances[0].customParameters[0].value, 'CustomFamily') # the masters' family is also modified to use custom 'family_name' for ufo in ufos: self.assertEqual(ufo.info.familyName, 'CustomFamily') def test_lib_no_weight(self): font = generate_minimal_font() ufo = to_ufos(font)[0] self.assertEqual(ufo.lib[GLYPHS_PREFIX + 'weight'], 'Regular') def test_lib_weight(self): font = generate_minimal_font() font.masters[0].weight = 'Bold' ufo = to_ufos(font)[0] self.assertEqual(ufo.lib[GLYPHS_PREFIX + 'weight'], 'Bold') def test_lib_no_width(self): font = generate_minimal_font() ufo = to_ufos(font)[0] self.assertEqual(ufo.lib[GLYPHS_PREFIX + 'width'], 'Regular') def test_lib_width(self): font = generate_minimal_font() font.masters[0].width = 'Condensed' ufo = to_ufos(font)[0] self.assertEqual(ufo.lib[GLYPHS_PREFIX + 'width'], 'Condensed') def test_lib_no_custom(self): font = generate_minimal_font() ufo = to_ufos(font)[0] self.assertFalse(GLYPHS_PREFIX + 'customName' in ufo.lib) def test_lib_custom(self): font = generate_minimal_font() font.masters[0].customName = 'FooBar' ufo = to_ufos(font)[0] self.assertEqual(ufo.lib[GLYPHS_PREFIX + 'customName'], 'FooBar') def test_coerce_to_bool(self): font = generate_minimal_font() font.customParameters['Disable Last Change'] = 'Truthy' ufo = to_ufos(font)[0] self.assertEqual(True, ufo.lib[GLYPHS_PREFIX + 'disablesLastChange']) def _run_guideline_test(self, data_in, expected): font = generate_minimal_font() glyph = GSGlyph(name='a') font.glyphs.append(glyph) layer = GSLayer() layer.layerId = font.masters[0].id layer.width = 0 for guide_data in data_in: pt = point(value=guide_data['position'][0], value2=guide_data['position'][1]) guide = GSGuideLine() guide.position = pt guide.angle = guide_data['angle'] layer.guides.append(guide) glyph.layers.append(layer) ufo = to_ufos(font)[0] self.assertEqual(ufo['a'].guidelines, expected) def test_set_guidelines(self): """Test that guidelines are set correctly.""" self._run_guideline_test( [{'position': (1, 2), 'angle': 270}], [{str('x'): 1, str('y'): 2, str('angle'): 90}]) def test_set_guidelines_duplicates(self): """Test that duplicate guidelines are accepted.""" self._run_guideline_test( [{'position': (1, 2), 'angle': 270}, {'position': (1, 2), 'angle': 270}], [{str('x'): 1, str('y'): 2, str('angle'): 90}, {str('x'): 1, str('y'): 2, str('angle'): 90}]) # TODO test more than just name def test_supplementary_layers(self): """Test sub layers.""" font = generate_minimal_font() glyph = GSGlyph(name="a") font.glyphs.append(glyph) layer = GSLayer() layer.layerId = font.masters[0].id layer.width = 0 glyph.layers.append(layer) sublayer = GSLayer() sublayer.associatedMasterId = font.masters[0].id sublayer.width = 0 sublayer.name = "SubLayer" glyph.layers.append(sublayer) ufo = to_ufos(font)[0] self.assertEqual( [l.name for l in ufo.layers], ["public.default", "SubLayer"] ) def test_glyph_lib_Export(self): font = generate_minimal_font() glyph = add_glyph(font, "a") self.assertEqual(glyph.export, True) ufo = to_ufos(font)[0] self.assertNotIn(GLYPHLIB_PREFIX + "Export", ufo["a"].lib) glyph.export = False ufo = to_ufos(font)[0] self.assertEqual(ufo["a"].lib[GLYPHLIB_PREFIX + "Export"], False) def test_glyph_lib_metricsKeys(self): font = generate_minimal_font() glyph = add_glyph(font, "x") glyph.leftMetricsKey = "y" glyph.rightMetricsKey = "z" assert glyph.widthMetricsKey is None ufo = to_ufos(font)[0] self.assertEqual(ufo["x"].lib[GLYPHLIB_PREFIX + "leftMetricsKey"], "y") self.assertEqual(ufo["x"].lib[GLYPHLIB_PREFIX + "rightMetricsKey"], "z") self.assertNotIn(GLYPHLIB_PREFIX + "widthMetricsKey", ufo["x"].lib) def test_glyph_lib_componentsAlignment_and_componentsLocked(self): font = generate_minimal_font() add_glyph(font, "a") add_glyph(font, "b") composite_glyph = add_glyph(font, "c") add_component(font, "c", "a", (1, 0, 0, 1, 0, 0)) add_component(font, "c", "b", (1, 0, 0, 1, 0, 100)) comp1 = composite_glyph.layers[0].components[0] comp2 = composite_glyph.layers[0].components[1] self.assertEqual(comp1.alignment, 0) self.assertEqual(comp1.locked, False) ufo = to_ufos(font)[0] # all components have deault values, no lib key is written self.assertNotIn(GLYPHS_PREFIX + "componentsAlignment", ufo["c"].lib) self.assertNotIn(GLYPHS_PREFIX + "componentsLocked", ufo["c"].lib) comp2.alignment = -1 comp1.locked = True ufo = to_ufos(font)[0] # if any component has a non-default alignment/locked values, write # list of values for all of them self.assertIn(GLYPHS_PREFIX + "componentsAlignment", ufo["c"].lib) self.assertEqual( ufo["c"].lib[GLYPHS_PREFIX + "componentsAlignment"], [0, -1]) self.assertIn(GLYPHS_PREFIX + "componentsLocked", ufo["c"].lib) self.assertEqual( ufo["c"].lib[GLYPHS_PREFIX + "componentsLocked"], [True, False]) class _PointDataPen(object): def __init__(self): self.contours = [] def addPoint(self, pt, segmentType=None, smooth=False, **kwargs): self.contours[-1].append((pt[0], pt[1], segmentType, smooth)) def beginPath(self): self.contours.append([]) def endPath(self): if not self.contours[-1]: self.contours.pop() def addComponent(self, *args, **kwargs): pass class DrawPathsTest(unittest.TestCase): def test_to_ufo_draw_paths_empty_nodes(self): contours = [GSPath()] pen = _PointDataPen() to_ufo_draw_paths(None, pen, contours) self.assertEqual(pen.contours, []) def test_to_ufo_draw_paths_open(self): path = GSPath() path.nodes = [ GSNode(position=(0, 0), nodetype='line'), GSNode(position=(1, 1), nodetype='offcurve'), GSNode(position=(2, 2), nodetype='offcurve'), GSNode(position=(3, 3), nodetype='curve', smooth=True), ] path.closed = False pen = _PointDataPen() to_ufo_draw_paths(None, pen, [path]) self.assertEqual(pen.contours, [[ (0, 0, 'move', False), (1, 1, None, False), (2, 2, None, False), (3, 3, 'curve', True), ]]) def test_to_ufo_draw_paths_closed(self): path = GSPath() path.nodes = [ GSNode(position=(0, 0), nodetype='offcurve'), GSNode(position=(1, 1), nodetype='offcurve'), GSNode(position=(2, 2), nodetype='curve', smooth=True), GSNode(position=(3, 3), nodetype='offcurve'), GSNode(position=(4, 4), nodetype='offcurve'), GSNode(position=(5, 5), nodetype='curve', smooth=True), ] path.closed = True pen = _PointDataPen() to_ufo_draw_paths(None, pen, [path]) points = pen.contours[0] first_x, first_y = points[0][:2] self.assertEqual((first_x, first_y), (5, 5)) first_segment_type = points[0][2] self.assertEqual(first_segment_type, 'curve') def test_to_ufo_draw_paths_qcurve(self): path = GSPath() path.nodes = [ GSNode(position=(143, 695), nodetype='offcurve'), GSNode(position=(37, 593), nodetype='offcurve'), GSNode(position=(37, 434), nodetype='offcurve'), GSNode(position=(143, 334), nodetype='offcurve'), GSNode(position=(223, 334), nodetype='qcurve', smooth=True), ] path.closed = True pen = _PointDataPen() to_ufo_draw_paths(None, pen, [path]) points = pen.contours[0] first_x, first_y = points[0][:2] self.assertEqual((first_x, first_y), (223, 334)) first_segment_type = points[0][2] self.assertEqual(first_segment_type, 'qcurve') class GlyphPropertiesTest(unittest.TestCase): def test_glyph_color(self): font = generate_minimal_font() glyph = GSGlyph(name='a') glyph2 = GSGlyph(name='b') glyph3 = GSGlyph(name='c') glyph4 = GSGlyph(name='d') glyph.color = [244, 0, 138, 1] glyph2.color = 3 glyph3.color = 88 glyph4.color = [800, 0, 138, 1] font.glyphs.append(glyph) font.glyphs.append(glyph2) font.glyphs.append(glyph3) font.glyphs.append(glyph4) layer = GSLayer() layer2 = GSLayer() layer3 = GSLayer() layer4 = GSLayer() layer.layerId = font.masters[0].id layer2.layerId = font.masters[0].id layer3.layerId = font.masters[0].id layer4.layerId = font.masters[0].id glyph.layers.append(layer) glyph2.layers.append(layer2) glyph3.layers.append(layer3) glyph4.layers.append(layer4) ufo = to_ufos(font)[0] self.assertEqual(ufo['a'].lib.get('public.markColor'), '0.9569,0,0.5412,0.0039') self.assertEqual(ufo['b'].lib.get('public.markColor'), '0.97,1,0,1') self.assertEqual(ufo['c'].lib.get('public.markColor'), None) self.assertEqual(ufo['d'].lib.get('public.markColor'), None) class SkipDanglingAndNamelessLayers(unittest.TestCase): def setUp(self): self.font = generate_minimal_font() add_glyph(self.font, "a") self.logger = logging.getLogger("glyphsLib.builder.builders.UFOBuilder") def test_normal_layer(self): with CapturingLogHandler(self.logger, level="WARNING") as captor: to_ufos(self.font) # no warnings are emitted self.assertRaises( AssertionError, captor.assertRegex, "is dangling and will be skipped") self.assertRaises( AssertionError, captor.assertRegex, "layer without a name") def test_nameless_layer(self): self.font.glyphs[0].layers[0].associatedMasterId = "xxx" with CapturingLogHandler(self.logger, level="WARNING") as captor: to_ufos(self.font) captor.assertRegex("layer without a name") def test_dangling_layer(self): self.font.glyphs[0].layers[0].layerId = "yyy" self.font.glyphs[0].layers[0].associatedMasterId = "xxx" with CapturingLogHandler(self.logger, level="WARNING") as captor: to_ufos(self.font) captor.assertRegex("is dangling and will be skipped") if __name__ == '__main__': unittest.main() glyphslib-2.2.1/tests/classes_test.py000077500000000000000000001465371322341616200177440ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import ( print_function, division, absolute_import, unicode_literals) import os import datetime import unittest import copy from fontTools.misc.py23 import unicode from glyphsLib.classes import ( GSFont, GSFontMaster, GSInstance, GSCustomParameter, GSGlyph, GSLayer, GSAnchor, GSComponent, GSAlignmentZone, GSClass, GSFeature, GSAnnotation, GSFeaturePrefix, GSGuideLine, GSHint, GSNode, GSSmartComponentAxis, LayerComponentsProxy, LayerGuideLinesProxy, STEM, TEXT, ARROW, CIRCLE, PLUS, MINUS ) from glyphsLib.types import point, transform, rect, size TESTFILE_PATH = os.path.join( os.path.dirname(__file__), os.path.join('data', 'GlyphsUnitTestSans.glyphs') ) def generate_minimal_font(): font = GSFont() font.appVersion = 895 font.date = datetime.datetime.today() font.familyName = 'MyFont' master = GSFontMaster() master.ascender = 0 master.capHeight = 0 master.descender = 0 master.id = 'id' master.xHeight = 0 font.masters.append(master) font.upm = 1000 font.versionMajor = 1 font.versionMinor = 0 return font def generate_instance_from_dict(instance_dict): instance = GSInstance() instance.name = instance_dict['name'] for custom_parameter in instance_dict.get('customParameters', []): cp = GSCustomParameter() cp.name = custom_parameter['name'] cp.value = custom_parameter['value'] instance.customParameters.append(cp) return instance def add_glyph(font, glyphname): glyph = GSGlyph() glyph.name = glyphname font.glyphs.append(glyph) layer = GSLayer() layer.layerId = font.masters[0].id layer.associatedMasterId = font.masters[0].id layer.width = 0 glyph.layers.append(layer) return glyph def add_anchor(font, glyphname, anchorname, x, y): for glyph in font.glyphs: if glyph.name == glyphname: for master in font.masters: layer = glyph.layers[master.id] layer.anchors = getattr(layer, 'anchors', []) anchor = GSAnchor() anchor.name = anchorname anchor.position = (x, y) layer.anchors.append(anchor) def add_component(font, glyphname, componentname, transform): for glyph in font.glyphs: if glyph.name == glyphname: for layer in glyph.layers.values(): component = GSComponent() component.name = componentname component.transform = transform layer.components.append(component) class GlyphLayersTest(unittest.TestCase): def test_check_master_layer(self): font = generate_minimal_font() glyph = add_glyph(font, "A") self.assertIsNotNone(glyph) master = font.masters[0] self.assertIsNotNone(master) layer = glyph.layers[master.id] self.assertIsNotNone(layer) layer = glyph.layers["XYZ123"] self.assertIsNone(layer) class GSFontTest(unittest.TestCase): def test_init(self): font = GSFont() self.assertEqual(font.familyName, "Unnamed font") self.assertEqual(font.versionMajor, 1) self.assertEqual(font.versionMinor, 0) self.assertEqual(font.appVersion, "895") self.assertEqual(len(font.glyphs), 0) self.assertEqual(len(font.masters), 0) self.assertEqual(list(font.masters), list(())) self.assertEqual(len(font.instances), 0) self.assertEqual(font.instances, []) self.assertEqual(len(font.customParameters), 0) def test_repr(self): font = GSFont() self.assertEqual(repr(font), '') def test_update_custom_parameter(self): font = GSFont() font.customParameters['Filter'] = 'RemoveOverlap' self.assertEqual(font.customParameters['Filter'], 'RemoveOverlap') font.customParameters['Filter'] = 'AddExtremes' self.assertEqual(font.customParameters['Filter'], 'AddExtremes') def test_font_master_proxy(self): font = GSFont() master = GSFontMaster() font.masters.append(master) self.assertEqual(master.font, font) class GSObjectsTestCase(unittest.TestCase): def setUp(self): self.font = GSFont(TESTFILE_PATH) def assertString(self, value): self.assertIsInstance(value, str) old_value = value value = "eee" self.assertEqual(value, "eee") value = old_value self.assertEqual(value, old_value) def assertUnicode(self, value): self.assertIsInstance(value, unicode) old_value = value value = "ə" self.assertEqual(value, "ə") value = old_value self.assertEqual(value, old_value) def assertInteger(self, value): self.assertIsInstance(value, int) old_value = value value = 5 self.assertEqual(value, 5) value = old_value self.assertEqual(value, old_value) def assertFloat(self, value): self.assertIsInstance(value, float) old_value = value value = 0.5 self.assertEqual(value, 0.5) value = old_value self.assertEqual(value, old_value) def assertBool(self, value): self.assertIsInstance(value, bool) old_value = value value = not value self.assertEqual(value, not old_value) value = old_value self.assertEqual(value, old_value) def assertDict(self, dictObject): self.assertIsInstance(dictObject, dict) var1 = 'abc' var2 = 'def' dictObject['uniTestValue'] = var1 self.assertEqual(dictObject['uniTestValue'], var1) dictObject['uniTestValue'] = var2 self.assertEqual(dictObject['uniTestValue'], var2) class GSFontFromFileTest(GSObjectsTestCase): def setUp(self): super(GSFontFromFileTest, self).setUp() def test_masters(self): font = self.font amount = len(font.masters) self.assertEqual(len(list(font.masters)), 3) new_master = GSFontMaster() font.masters.append(new_master) self.assertEqual(new_master, font.masters[-1]) del font.masters[-1] new_master1 = GSFontMaster() new_master2 = GSFontMaster() font.masters.extend([new_master1, new_master2]) self.assertEqual(new_master1, font.masters[-2]) self.assertEqual(new_master2, font.masters[-1]) font.masters.remove(font.masters[-1]) font.masters.remove(font.masters[-1]) new_master = GSFontMaster() font.masters.insert(0, new_master) self.assertEqual(new_master, font.masters[0]) font.masters.remove(font.masters[0]) self.assertEqual(amount, len(font.masters)) def test_instances(self): font = self.font amount = len(font.instances) self.assertEqual(len(list(font.instances)), 8) new_instance = GSInstance() font.instances.append(new_instance) self.assertEqual(new_instance, font.instances[-1]) del font.instances[-1] new_instance1 = GSInstance() new_instance2 = GSInstance() font.instances.extend([new_instance1, new_instance2]) self.assertEqual(new_instance1, font.instances[-2]) self.assertEqual(new_instance2, font.instances[-1]) font.instances.remove(font.instances[-1]) font.instances.remove(font.instances[-1]) new_instance = GSInstance() font.instances.insert(0, new_instance) self.assertEqual(new_instance, font.instances[0]) font.instances.remove(font.instances[0]) self.assertEqual(amount, len(font.instances)) def test_glyphs(self): font = self.font self.assertGreaterEqual(len(list(font.glyphs)), 1) by_index = font.glyphs[3] by_name = font.glyphs['adieresis'] by_unicode_char = font.glyphs['ä'] by_unicode_value = font.glyphs['00E4'] by_unicode_value_lowercased = font.glyphs['00e4'] self.assertEqual(by_index, by_name) self.assertEqual(by_unicode_char, by_name) self.assertEqual(by_unicode_value, by_name) self.assertEqual(by_unicode_value_lowercased, by_name) def test_classes(self): font = self.font font.classes = [] amount = len(font.classes) font.classes.append(GSClass('uppercaseLetters', 'A')) self.assertIsNotNone(font.classes[-1].__repr__()) self.assertEqual(len(font.classes), 1) self.assertIn('', str(font.classes)) self.assertIn('A', font.classes['uppercaseLetters'].code) del(font.classes['uppercaseLetters']) newClass1 = GSClass('uppercaseLetters1', 'A') newClass2 = GSClass('uppercaseLetters2', 'A') font.classes.extend([newClass1, newClass2]) self.assertEqual(newClass1, font.classes[-2]) self.assertEqual(newClass2, font.classes[-1]) newClass = GSClass('uppercaseLetters3', 'A') newClass = copy.copy(newClass) font.classes.insert(0, newClass) self.assertEqual(newClass, font.classes[0]) font.classes.remove(font.classes[-1]) font.classes.remove(font.classes[-1]) font.classes.remove(font.classes[0]) self.assertEqual(len(font.classes), amount) def test_features(self): font = self.font font.features = [] amount = len(font.features) font.features.append(GSFeature('liga', 'sub f i by fi;')) # TODO # self.assertIsNotNone(font.features['liga'].__repr__()) self.assertEqual(len(font.features), 1) # TODO # self.assertIn('', str(font.features)) # self.assertIn('sub f i by fi;', font.features['liga'].code) # del(font.features['liga']) del font.features[-1] newFeature1 = GSFeature('liga', 'sub f i by fi;') newFeature2 = GSFeature('liga', 'sub f l by fl;') font.features.extend([newFeature1, newFeature2]) self.assertEqual(newFeature1, font.features[-2]) self.assertEqual(newFeature2, font.features[-1]) newFeature = GSFeature('liga', 'sub f i by fi;') newFeature = copy.copy(newFeature) font.features.insert(0, newFeature) self.assertEqual(newFeature, font.features[0]) font.features.remove(font.features[-1]) font.features.remove(font.features[-1]) font.features.remove(font.features[0]) self.assertEqual(len(font.features), amount) def test_featurePrefixes(self): font = self.font font.featurePrefixes = [] amount = len(font.featurePrefixes) font.featurePrefixes.append( GSFeaturePrefix('LanguageSystems', 'languagesystem DFLT dflt;')) self.assertIsNotNone(font.featurePrefixes[-1].__repr__()) self.assertEqual(len(font.featurePrefixes), 1) self.assertIn('', str(font.featurePrefixes)) self.assertIn('languagesystem DFLT dflt;', font.featurePrefixes[-1].code) # TODO # del(font.featurePrefixes['LanguageSystems']) del font.featurePrefixes[-1] newFeaturePrefix1 = GSFeaturePrefix('LanguageSystems1', 'languagesystem DFLT dflt;') newFeaturePrefix2 = GSFeaturePrefix('LanguageSystems2', 'languagesystem DFLT dflt;') font.featurePrefixes.extend([newFeaturePrefix1, newFeaturePrefix2]) self.assertEqual(newFeaturePrefix1, font.featurePrefixes[-2]) self.assertEqual(newFeaturePrefix2, font.featurePrefixes[-1]) newFeaturePrefix = GSFeaturePrefix('LanguageSystems3', 'languagesystem DFLT dflt;') newFeaturePrefix = copy.copy(newFeaturePrefix) font.featurePrefixes.insert(0, newFeaturePrefix) self.assertEqual(newFeaturePrefix, font.featurePrefixes[0]) font.featurePrefixes.remove(font.featurePrefixes[-1]) font.featurePrefixes.remove(font.featurePrefixes[-1]) font.featurePrefixes.remove(font.featurePrefixes[0]) self.assertEqual(len(font.featurePrefixes), amount) def test_ints(self): attributes = [ "versionMajor", "versionMajor", "upm", "grid", "gridSubDivisions", ] font = self.font for attr in attributes: self.assertInteger(getattr(font, attr)) def test_strings(self): attributes = [ "copyright", "designer", "designerURL", "manufacturer", "manufacturerURL", "familyName", ] font = self.font for attr in attributes: self.assertUnicode(getattr(font, attr)) def test_note(self): font = self.font self.assertUnicode(font.note) # date def test_date(self): font = self.font self.assertIsInstance(font.date, datetime.datetime) def test_kerning(self): font = self.font self.assertDict(font.kerning) def test_userData(self): font = self.font self.assertEqual(font.userData["AsteriskParameters"], { "253E7231-480D-4F8E-8754-50FC8575C08E": [ "754", "30", 7, 51, "80", "50", ], }) # self.assertIsInstance(font.userData, dict) # TODO self.assertIsNone(font.userData["TestData"]) font.userData["TestData"] = 42 self.assertEqual(font.userData["TestData"], 42) self.assertTrue("TestData" in font.userData) del(font.userData["TestData"]) self.assertIsNone(font.userData["TestData"]) def test_disableNiceNames(self): font = self.font self.assertIsInstance(font.disablesNiceNames, bool) def test_customParameters(self): font = self.font font.customParameters['trademark'] = \ 'ThisFont is a trademark by MyFoundry.com' self.assertIn(font.customParameters['trademark'], 'ThisFont is a trademark by MyFoundry.com') amount = len(list(font.customParameters)) newParameter = GSCustomParameter('hello1', 'world1') font.customParameters.append(newParameter) self.assertEqual(newParameter, list(font.customParameters)[-1]) del font.customParameters[-1] newParameter1 = GSCustomParameter('hello2', 'world2') newParameter2 = GSCustomParameter('hello3', 'world3') newParameter2 = copy.copy(newParameter2) font.customParameters.extend([newParameter1, newParameter2]) self.assertEqual(newParameter1, list(font.customParameters)[-2]) self.assertEqual(newParameter2, list(font.customParameters)[-1]) font.customParameters.remove(list(font.customParameters)[-1]) font.customParameters.remove(list(font.customParameters)[-1]) newParameter = GSCustomParameter('hello1', 'world1') font.customParameters.insert(0, newParameter) self.assertEqual(newParameter, list(font.customParameters)[0]) font.customParameters.remove(list(font.customParameters)[0]) self.assertEqual(amount, len(list(font.customParameters))) del font.customParameters['trademark'] # TODO: selection, selectedLayers, currentText, tabs, currentTab # TODO: selectedFontMaster, masterIndex def test_filepath(self): font = self.font self.assertIsNotNone(font.filepath) # TODO: tool, tools # TODO: save(), close() # TODO: setKerningForPair(), kerningForPair(), removeKerningForPair() # TODO: updateFeatures() # TODO: copy(font) class GSFontMasterFromFileTest(GSObjectsTestCase): def setUp(self): super(GSFontMasterFromFileTest, self).setUp() self.font = GSFont(TESTFILE_PATH) self.master = self.font.masters[0] def test_attributes(self): master = self.master self.assertIsNotNone(master.__repr__()) self.assertIsNotNone(master.id) self.assertIsNotNone(master.name) self.assertIsNotNone(master.weight) self.assertIsNotNone(master.width) # weightValue obj = master.weightValue old_obj = obj self.assertIsInstance(obj, float) master.weightValue = 0.5 self.assertEqual(master.weightValue, 0.5) obj = old_obj self.assertIsInstance(master.weightValue, float) self.assertIsInstance(master.widthValue, float) self.assertIsInstance(master.customValue, float) self.assertIsInstance(master.ascender, float) self.assertIsInstance(master.capHeight, float) self.assertIsInstance(master.xHeight, float) self.assertIsInstance(master.descender, float) self.assertIsInstance(master.italicAngle, float) for attr in ["weightValue", "widthValue", "customValue", "ascender", "capHeight", "xHeight", "descender", "italicAngle"]: value = getattr(master, attr) self.assertIsInstance(value, float) setattr(master, attr, 0.5) self.assertEqual(getattr(master, attr), 0.5) setattr(master, attr, value) self.assertIsInstance(master.customName, unicode) # verticalStems oldStems = master.verticalStems master.verticalStems = [10, 15, 20] self.assertEqual(len(master.verticalStems), 3) master.verticalStems = oldStems # horizontalStems oldStems = master.horizontalStems master.horizontalStems = [10, 15, 20] self.assertEqual(len(master.horizontalStems), 3) master.horizontalStems = oldStems # alignmentZones self.assertIsInstance(master.alignmentZones, list) # TODO blueValues # self.assertIsInstance(master.blueValues, list) # TODO otherBlues # self.assertIsInstance(master.otherBlues, list) # guides self.assertIsInstance(master.guides, list) master.guides = [] self.assertEqual(len(master.guides), 0) newGuide = GSGuideLine() newGuide.position = point("{100, 100}") newGuide.angle = -10.0 master.guides.append(newGuide) self.assertIsNotNone(master.guides[0].__repr__()) self.assertEqual(len(master.guides), 1) del master.guides[0] self.assertEqual(len(master.guides), 0) # guides self.assertIsInstance(master.guides, list) master.guides = [] self.assertEqual(len(master.guides), 0) newGuide = GSGuideLine() newGuide.position = point("{100, 100}") newGuide.angle = -10.0 master.guides.append(newGuide) self.assertIsNotNone(master.guides[0].__repr__()) self.assertEqual(len(master.guides), 1) del master.guides[0] self.assertEqual(len(master.guides), 0) # userData self.assertIsNotNone(master.userData) master.userData["TestData"] = 42 self.assertEqual(master.userData["TestData"], 42) del master.userData["TestData"] # TODO # self.assertIsNone(master.userData["TestData"]) # customParameters master.customParameters['trademark'] = \ 'ThisFont is a trademark by MyFoundry.com' self.assertGreaterEqual(len(master.customParameters), 1) del(master.customParameters['trademark']) # font self.assertEqual(self.font, self.master.font) def test_name(self): master = self.master self.assertEqual('Light', master.name) master.customParameters['Master Name'] = 'My custom master name' self.assertEqual('My custom master name', master.name) del(master.customParameters['Master Name']) self.assertEqual('Light', master.name) master.italicAngle = 11 self.assertEqual('Light Italic', master.name) master.italicAngle = 0 master.customName = 'Rounded' self.assertEqual('Light Rounded', master.name) master.customName1 = 'Stretched' master.customName2 = 'Filled' master.customName3 = 'Rotated' self.assertEqual('Light Rounded Stretched Filled Rotated', master.name) master.customName1 = '' master.customName2 = '' self.assertEqual('Light Rounded Rotated', master.name) master.customName = '' master.customName3 = '' self.assertEqual('Light', master.name) class GSAlignmentZoneFromFileTest(GSObjectsTestCase): def setUp(self): super(GSAlignmentZoneFromFileTest, self).setUp() self.master = self.font.masters[0] def test_attributes(self): master = self.master for i, zone in enumerate([ (800, 10), (700, 10), (470, 10), (0, -10), (-200, -10)]): pos, size = zone self.assertEqual(master.alignmentZones[i].position, pos) self.assertEqual(master.alignmentZones[i].size, size) master.alignmentZones = [] self.assertEqual(len(master.alignmentZones), 0) master.alignmentZones.append(GSAlignmentZone(100, 10)) self.assertIsNotNone(master.alignmentZones[-1].__repr__()) zone = copy.copy(master.alignmentZones[-1]) self.assertEqual(len(master.alignmentZones), 1) self.assertEqual(master.alignmentZones[-1].position, 100) self.assertEqual(master.alignmentZones[-1].size, 10) del master.alignmentZones[-1] self.assertEqual(len(master.alignmentZones), 0) class GSInstanceFromFileTest(GSObjectsTestCase): def setUp(self): super(GSInstanceFromFileTest, self).setUp() self.instance = self.font.instances[0] def test_attributes(self): instance = self.instance self.assertIsNotNone(instance.__repr__()) # TODO: active # self.assertIsInstance(instance.active, bool) # name self.assertIsInstance(instance.name, unicode) # weight self.assertIsInstance(instance.weight, unicode) # width self.assertIsInstance(instance.width, unicode) # weightValue # widthValue # customValue for attr in ["weightValue", "widthValue", "customValue"]: value = getattr(instance, attr) self.assertIsInstance(value, float) setattr(instance, attr, 0.5) self.assertEqual(getattr(instance, attr), 0.5) setattr(instance, attr, value) # isItalic # isBold for attr in ["isItalic", "isBold"]: value = getattr(instance, attr) self.assertIsInstance(value, bool) setattr(instance, attr, not value) self.assertEqual(getattr(instance, attr), not value) setattr(instance, attr, value) # linkStyle self.assertIsInstance(instance.linkStyle, unicode) # familyName # preferredFamily # preferredSubfamilyName # windowsFamily # windowsStyle # windowsLinkedToStyle # fontName # fullName for attr in [ "familyName", "preferredFamily", "preferredSubfamilyName", "windowsFamily", "windowsStyle", "windowsLinkedToStyle", "fontName", "fullName", ]: # self.assertIsInstance(getattr(instance, attr), unicode) if not hasattr(instance, attr): print("instance does not have %s" % attr) if (hasattr(instance, "parent") and hasattr(instance.parent, attr)): value = getattr(instance.parent) print(value, type(value)) # customParameters instance.customParameters['trademark'] = \ 'ThisFont is a trademark by MyFoundry.com' self.assertGreaterEqual(len(instance.customParameters), 1) del(instance.customParameters['trademark']) # instanceInterpolations self.assertIsInstance(dict(instance.instanceInterpolations), dict) # manualInterpolation self.assertIsInstance(instance.manualInterpolation, bool) value = instance.manualInterpolation instance.manualInterpolation = not instance.manualInterpolation self.assertEqual(instance.manualInterpolation, not value) instance.manualInterpolation = value # interpolatedFont # TODO # self.assertIsInstance(instance.interpolatedFont, type(Glyphs.font)) # TODO generate() class GSGlyphFromFileTest(GSObjectsTestCase): def setUp(self): super(GSGlyphFromFileTest, self).setUp() self.glyph = self.font.glyphs['a'] # TODO duplicate # def test_duplicate(self): # font = self.font # glyph1 = self.glyph # glyph2 = glyph1.duplicate() # glyph3 = glyph1.duplicate('a.test') def test_parent(self): font = self.font glyph = self.glyph self.assertEqual(glyph.parent, font) def test_layers(self): glyph = self.glyph self.assertIsNotNone(glyph.layers) amount = len(glyph.layers) newLayer = GSLayer() newLayer.name = '1' glyph.layers.append(newLayer) self.assertIn('', str(glyph.layers[-1])) self.assertEqual(newLayer, glyph.layers[-1]) del glyph.layers[-1] newLayer1 = GSLayer() newLayer1.name = '2' newLayer2 = GSLayer() newLayer2.name = '3' glyph.layers.extend([newLayer1, newLayer2]) self.assertEqual(newLayer1, glyph.layers[-2]) self.assertEqual(newLayer2, glyph.layers[-1]) newLayer = GSLayer() newLayer.name = '4' # indices here don't make sense because layer get appended using a UUID glyph.layers.insert(0, newLayer) # so the latest layer got appended at the end also self.assertEqual(newLayer, glyph.layers[-1]) glyph.layers.remove(glyph.layers[-1]) glyph.layers.remove(glyph.layers[-1]) glyph.layers.remove(glyph.layers[-1]) self.assertEqual(amount, len(glyph.layers)) self.assertEqual( '[, , ' ', ]', repr(list(glyph.layers)), ) self.assertEqual( '[, , ' ', ]', repr(list(glyph.layers.values())), ) def test_layers_missing_master(self): ''' font.glyph['a'] has its layers in a different order than the font.masters and an extra layer. Adding a master but not adding it as a layer to the glyph should not affect glyph.layers unexpectedly. ''' glyph = self.glyph num_layers = len(glyph.layers) self.assertEqual(set(l.layerId for l in glyph.layers), set(l.layerId for l in glyph.layers.values())) self.assertNotEqual([l.layerId for l in glyph.layers], [l.layerId for l in glyph.layers.values()]) new_fontMaster = GSFontMaster() self.font.masters.insert(0, new_fontMaster) self.assertEqual(num_layers, len(glyph.layers)) self.assertEqual(set(l.layerId for l in glyph.layers), set(l.layerId for l in glyph.layers.values())) self.assertNotEqual([l.layerId for l in glyph.layers], [l.layerId for l in glyph.layers.values()]) def test_name(self): glyph = self.glyph self.assertIsInstance(glyph.name, unicode) value = glyph.name glyph.name = "Ə" self.assertEqual(glyph.name, "Ə") glyph.name = value def test_unicode(self): glyph = self.glyph self.assertIsInstance(glyph.unicode, unicode) value = glyph.unicode # TODO: # glyph.unicode = "004a" # self.assertEqual(glyph.unicode, "004A") glyph.unicode = "004B" self.assertEqual(glyph.unicode, "004B") glyph.unicode = value def test_string(self): glyph = self.font.glyphs["adieresis"] self.assertEqual(glyph.string, "ä") def test_id(self): # TODO pass # TODO # category # storeCategory # subCategory # storeSubCategory # script # storeScript # productionName # storeProductionName # glyphInfo def test_horiz_kerningGroup(self): for group in ["leftKerningGroup", "rightKerningGroup"]: glyph = self.glyph self.assertIsInstance(getattr(glyph, group), unicode) value = getattr(glyph, group) setattr(glyph, group, "ä") self.assertEqual(getattr(glyph, group), "ä") setattr(glyph, group, value) def test_horiz_metricsKey(self): for group in ["leftMetricsKey", "rightMetricsKey"]: glyph = self.glyph if getattr(glyph, group) is not None: self.assertIsInstance(getattr(glyph, group), unicode) value = getattr(glyph, group) setattr(glyph, group, "ä") self.assertEqual(getattr(glyph, group), "ä") setattr(glyph, group, value) def test_export(self): glyph = self.glyph self.assertIsInstance(glyph.export, bool) value = glyph.export glyph.export = not glyph.export self.assertEqual(glyph.export, not value) glyph.export = value def test_color(self): glyph = self.glyph if glyph.color is not None: self.assertIsInstance(glyph.color, int) value = glyph.color glyph.color = 5 self.assertEqual(glyph.color, 5) glyph.color = value def test_note(self): glyph = self.glyph if glyph.note is not None: self.assertIsInstance(glyph.note, unicode) value = glyph.note glyph.note = "ä" self.assertEqual(glyph.note, "ä") glyph.note = value # TODO # masterCompatible def test_userData(self): glyph = self.glyph # self.assertIsNone(glyph.userData) amount = len(glyph.userData) var1 = "abc" var2 = "def" glyph.userData["unitTestValue"] = var1 self.assertEqual(glyph.userData["unitTestValue"], var1) glyph.userData["unitTestValue"] = var2 self.assertEqual(glyph.userData["unitTestValue"], var2) del glyph.userData["unitTestValue"] self.assertIsNone(glyph.userData.get("unitTestValue")) self.assertEqual(len(glyph.userData), amount) def test_smart_component_axes(self): shoulder = self.font.glyphs['_part.shoulder'] axes = shoulder.smartComponentAxes self.assertIsNotNone(axes) crotch_depth, shoulder_width = axes self.assertIsInstance(crotch_depth, GSSmartComponentAxis) self.assertEqual("crotchDepth", crotch_depth.name) self.assertEqual(0, crotch_depth.topValue) self.assertEqual(-100, crotch_depth.bottomValue) self.assertIsInstance(shoulder_width, GSSmartComponentAxis) self.assertEqual("shoulderWidth", shoulder_width.name) self.assertEqual(100, shoulder_width.topValue) self.assertEqual(0, shoulder_width.bottomValue) # TODO # lastChange class GSLayerFromFileTest(GSObjectsTestCase): def setUp(self): super(GSLayerFromFileTest, self).setUp() self.glyph = self.font.glyphs["a"] self.layer = self.glyph.layers[0] def test_repr(self): layer = self.layer self.assertIsNotNone(layer.__repr__()) def test_parent(self): self.assertEqual(self.layer.parent, self.glyph) def test_name(self): layer = self.layer self.assertUnicode(layer.name) # TODO # def test_associatedMasterId(self): # font = self.font # layer = self.layer # self.assertEqual(layer.associatedMasterId, font.masters[0].id) # def test_layerId(self): # font = self.font # layer = self.layer # self.assertEqual(layer.layerId, font.masters[0].id) # TODO set layer color in .glyphs file # def test_color(self): # layer = self.layer # self.assertInteger(layer.color) def test_components(self): glyph = self.font.glyphs["adieresis"] layer = glyph.layers[0] self.assertIsNotNone(layer.components) self.assertIsInstance(layer.components, LayerComponentsProxy) # self.assertGreaterEqual(len(layer.components), 1) self.assertEqual(len(layer.components), 2) # for component in layer.components: # self.assertIsInstance(component, GSComponent) # self.assertEqual(component.parent, layer) amount = len(layer.components) component = GSComponent() component.name = "A" layer.components.append(component) self.assertEqual(component.parent, layer) self.assertEqual(len(layer.components), amount + 1) del layer.components[-1] self.assertEqual(len(layer.components), amount) layer.components.extend([component]) self.assertEqual(len(layer.components), amount + 1) layer.components.remove(component) self.assertEqual(len(layer.components), amount) def test_guides(self): layer = self.layer self.assertIsInstance(layer.guides, LayerGuideLinesProxy) for guide in layer.guides: self.assertEqual(guide.parent, layer) layer.guides = [] self.assertEqual(len(layer.guides), 0) newGuide = GSGuideLine() newGuide.position = point("{100, 100}") newGuide.angle = -10.0 amount = len(layer.guides) layer.guides.append(newGuide) self.assertEqual(newGuide.parent, layer) self.assertIsNotNone(layer.guides[0].__repr__()) self.assertEqual(len(layer.guides), amount + 1) del layer.guides[0] self.assertEqual(len(layer.guides), amount) def test_annotations(self): layer = self.layer # self.assertEqual(layer.annotations, []) self.assertEqual(len(layer.annotations), 0) newAnnotation = GSAnnotation() newAnnotation.type = TEXT newAnnotation.text = 'This curve is ugly!' layer.annotations.append(newAnnotation) # TODO position.x, position.y # self.assertIsNotNone(layer.annotations[0].__repr__()) self.assertEqual(len(layer.annotations), 1) del layer.annotations[0] self.assertEqual(len(layer.annotations), 0) newAnnotation1 = GSAnnotation() newAnnotation1.type = ARROW newAnnotation2 = GSAnnotation() newAnnotation2.type = CIRCLE newAnnotation3 = GSAnnotation() newAnnotation3.type = PLUS layer.annotations.extend([newAnnotation1, newAnnotation2, newAnnotation3]) self.assertEqual(layer.annotations[-3], newAnnotation1) self.assertEqual(layer.annotations[-2], newAnnotation2) self.assertEqual(layer.annotations[-1], newAnnotation3) newAnnotation = GSAnnotation() newAnnotation = copy.copy(newAnnotation) newAnnotation.type = MINUS layer.annotations.insert(0, newAnnotation) self.assertEqual(layer.annotations[0], newAnnotation) layer.annotations.remove(layer.annotations[0]) layer.annotations.remove(layer.annotations[-1]) layer.annotations.remove(layer.annotations[-1]) layer.annotations.remove(layer.annotations[-1]) self.assertEqual(len(layer.annotations), 0) def test_hints_from_file(self): glyph = self.font.glyphs["A"] layer = glyph.layers[1] self.assertEqual(2, len(layer.hints)) first, second = layer.hints self.assertIsInstance(first, GSHint) self.assertTrue(first.horizontal) self.assertIsInstance(first.originNode, GSNode) first_origin_node = layer.paths[1].nodes[1] self.assertEqual(first_origin_node, first.originNode) self.assertIsInstance(second, GSHint) second_target_node = layer.paths[0].nodes[4] self.assertEqual(second_target_node, second.targetNode) def test_hints(self): layer = self.layer # layer.hints = [] self.assertEqual(len(layer.hints), 0) newHint = GSHint() newHint = copy.copy(newHint) newHint.originNode = layer.paths[0].nodes[0] newHint.targetNode = layer.paths[0].nodes[1] newHint.type = STEM layer.hints.append(newHint) self.assertIsNotNone(layer.hints[0].__repr__()) self.assertEqual(len(layer.hints), 1) del layer.hints[0] self.assertEqual(len(layer.hints), 0) newHint1 = GSHint() newHint1.originNode = layer.paths[0].nodes[0] newHint1.targetNode = layer.paths[0].nodes[1] newHint1.type = STEM newHint2 = GSHint() newHint2.originNode = layer.paths[0].nodes[0] newHint2.targetNode = layer.paths[0].nodes[1] newHint2.type = STEM layer.hints.extend([newHint1, newHint2]) newHint = GSHint() newHint.originNode = layer.paths[0].nodes[0] newHint.targetNode = layer.paths[0].nodes[1] self.assertEqual(layer.hints[-2], newHint1) self.assertEqual(layer.hints[-1], newHint2) layer.hints.insert(0, newHint) self.assertEqual(layer.hints[0], newHint) layer.hints.remove(layer.hints[0]) layer.hints.remove(layer.hints[-1]) layer.hints.remove(layer.hints[-1]) self.assertEqual(len(layer.hints), 0) def test_anchors(self): layer = self.layer amount = len(layer.anchors) self.assertEqual(len(layer.anchors), 3) for anchor in layer.anchors: self.assertEqual(anchor.parent, layer) if layer.anchors['top']: oldPosition = layer.anchors['top'].position else: oldPosition = None layer.anchors['top'] = GSAnchor() self.assertGreaterEqual(len(layer.anchors), 1) self.assertIsNotNone(layer.anchors['top'].__repr__()) layer.anchors['top'].position = point("{100, 100}") # anchor = copy.copy(layer.anchors['top']) del layer.anchors['top'] layer.anchors['top'] = GSAnchor() self.assertEqual(amount, len(layer.anchors)) layer.anchors['top'].position = oldPosition self.assertUnicode(layer.anchors['top'].name) newAnchor1 = GSAnchor() newAnchor1.name = 'testPosition1' newAnchor2 = GSAnchor() newAnchor2.name = 'testPosition2' layer.anchors.extend([newAnchor1, newAnchor2]) self.assertEqual(layer.anchors['testPosition1'], newAnchor1) self.assertEqual(layer.anchors['testPosition2'], newAnchor2) newAnchor3 = GSAnchor() newAnchor3.name = 'testPosition3' layer.anchors.append(newAnchor3) self.assertEqual(layer.anchors['testPosition3'], newAnchor3) layer.anchors.remove(layer.anchors['testPosition3']) layer.anchors.remove(layer.anchors['testPosition2']) layer.anchors.remove(layer.anchors['testPosition1']) self.assertEqual(amount, len(layer.anchors)) # TODO layer.paths # TODO # selection # TODO # LSB, RSB, TSB, BSB, width def test_leftMetricsKey(self): self.assertIs(self.layer.leftMetricsKey, None) def test_rightMetricsKey(self): self.assertIs(self.layer.rightMetricsKey, None) def test_widthMetricsKey(self): self.assertIs(self.layer.widthMetricsKey, None) # TODO: bounds, selectionBounds def test_background(self): self.assertIn('GSBackgroundLayer', self.layer.background.__repr__()) # TODO? # bezierPath, openBezierPath, completeBezierPath, completeOpenBezierPath? def test_userData(self): layer = self.layer # self.assertDict(layer.userData) layer.userData["Hallo"] = "Welt" self.assertEqual(layer.userData["Hallo"], "Welt") self.assertTrue("Hallo" in layer.userData) def test_smartComponentPoleMapping(self): # http://docu.glyphsapp.com/#smartComponentPoleMapping # Read some data from the file shoulder = self.font.glyphs["_part.shoulder"] for layer in shoulder.layers: if layer.name == "NarrowShoulder": mapping = layer.smartComponentPoleMapping self.assertIsNotNone(mapping) # crotchDepth is at the top pole self.assertEqual(2, mapping["crotchDepth"]) # shoulderWidth is at the bottom pole self.assertEqual(1, mapping["shoulderWidth"]) # Exercise the getter/setter layer = self.layer self.assertDict(layer.smartComponentPoleMapping) self.assertFalse("crotchDepth" in layer.smartComponentPoleMapping) layer.smartComponentPoleMapping["crotchDepth"] = 2 self.assertTrue("crotchDepth" in layer.smartComponentPoleMapping) layer.smartComponentPoleMapping = {"shoulderWidth": 1} self.assertFalse("crotchDepth" in layer.smartComponentPoleMapping) self.assertEqual(1, layer.smartComponentPoleMapping["shoulderWidth"]) # TODO: Methods # copyDecomposedLayer() # decomposeComponents() # compareString() # connectAllOpenPaths() # syncMetrics() # correctPathDirection() # removeOverlap() # roundCoordinates() # addNodesAtExtremes() # applyTransform() # beginChanges() # endChanges() # cutBetweenPoints() # intersectionsBetweenPoints() # addMissingAnchors() # clearSelection() # swapForegroundWithBackground() # reinterpolate() # clear() class GSComponentFromFileTest(GSObjectsTestCase): def setUp(self): super(GSComponentFromFileTest, self).setUp() self.glyph = self.font.glyphs["adieresis"] self.layer = self.glyph.layers[0] self.component = self.layer.components[0] def test_repr(self): component = self.component self.assertIsNotNone(component.__repr__()) self.assertEqual(repr(component), '') def test_delete_and_add(self): layer = self.layer self.assertEqual(len(layer.components), 2) layer.components = [] self.assertEqual(len(layer.components), 0) layer.components.append(GSComponent('a')) self.assertIsNotNone(layer.components[0].__repr__()) self.assertEqual(len(layer.components), 1) layer.components.append(GSComponent('dieresis')) self.assertEqual(len(layer.components), 2) layer.components = [GSComponent('a'), GSComponent('dieresis')] self.assertEqual(len(layer.components), 2) layer.components = [] layer.components.extend([GSComponent('a'), GSComponent('dieresis')]) self.assertEqual(len(layer.components), 2) newComponent = GSComponent('dieresis') layer.components.insert(0, newComponent) self.assertEqual(newComponent, layer.components[0]) layer.components.remove(layer.components[0]) self.assertEqual(len(layer.components), 2) def test_position(self): self.assertIsInstance(self.component.position, point) def test_componentName(self): self.assertUnicode(self.component.componentName) def test_component(self): self.assertIsInstance(self.component.component, GSGlyph) def test_rotation(self): self.assertFloat(self.component.rotation) def test_transform(self): self.assertIsInstance(self.component.transform, transform) self.assertEqual(len(self.component.transform.value), 6) def test_bounds(self): self.assertIsInstance(self.component.bounds, rect) bounds = self.component.bounds self.assertEqual(bounds.origin.x, 80) self.assertEqual(bounds.origin.y, -10) self.assertEqual(bounds.size.width, 289) self.assertEqual(bounds.size.height, 490) def test_moreBounds(self): self.component.scale = 1.1 bounds = self.component.bounds self.assertEqual(bounds.origin.x, 88) self.assertEqual(bounds.origin.y, -11) self.assertEqual(round(bounds.size.width * 10), round(317.9 * 10)) self.assertEqual(round(bounds.size.height * 10), round(539 * 10)) # def test_automaticAlignment(self): # self.assertBool(self.component.automaticAlignment) def test_anchor(self): self.assertString(self.component.anchor) def test_smartComponentValues(self): glyph = self.font.glyphs["h"] stem, shoulder = glyph.layers[0].components self.assertEqual(100, stem.smartComponentValues["height"]) self.assertEqual(-80.20097, shoulder.smartComponentValues["crotchDepth"]) self.assertNotIn("shoulderWidth", shoulder.smartComponentValues) self.assertNotIn("somethingElse", shoulder.smartComponentValues) # bezierPath? # componentLayer() class GSGuideLineTest(unittest.TestCase): def test_repr(self): guide = GSGuideLine() self.assertEqual(repr(guide), '') class GSAnchorFromFileTest(GSObjectsTestCase): def setUp(self): super(GSAnchorFromFileTest, self).setUp() self.glyph = self.font.glyphs["a"] self.layer = self.glyph.layers[0] self.anchor = self.layer.anchors[0] def test_repr(self): anchor = self.anchor self.assertEqual(anchor.__repr__(), '') def test_name(self): anchor = self.anchor self.assertUnicode(anchor.name) # TODO def test_position(self): pass class GSPathFromFileTest(GSObjectsTestCase): def setUp(self): super(GSPathFromFileTest, self).setUp() self.glyph = self.font.glyphs["a"] self.layer = self.glyph.layers[0] self.path = self.layer.paths[0] def test_proxy(self): layer = self.layer path = self.path amount = len(layer.paths) pathCopy1 = copy.copy(path) layer.paths.append(pathCopy1) pathCopy2 = copy.copy(pathCopy1) layer.paths.extend([pathCopy2]) self.assertEqual(layer.paths[-2], pathCopy1) self.assertEqual(layer.paths[-1], pathCopy2) pathCopy3 = copy.copy(pathCopy2) layer.paths.insert(0, pathCopy3) self.assertEqual(layer.paths[0], pathCopy3) layer.paths.remove(layer.paths[0]) layer.paths.remove(layer.paths[-1]) layer.paths.remove(layer.paths[-1]) self.assertEqual(amount, len(layer.paths)) def test_parent(self): path = self.path self.assertEqual(path.parent, self.layer) def test_nodes(self): path = self.path self.assertIsNotNone(path.nodes) self.assertEqual(len(path.nodes), 44) for node in path.nodes: self.assertEqual(node.parent, path) amount = len(path.nodes) newNode = GSNode(point("{100, 100}")) path.nodes.append(newNode) self.assertEqual(newNode, path.nodes[-1]) del path.nodes[-1] newNode = GSNode(point("{20, 20}")) path.nodes.insert(0, newNode) self.assertEqual(newNode, path.nodes[0]) path.nodes.remove(path.nodes[0]) newNode1 = GSNode(point("{10, 10}")) newNode2 = GSNode(point("{20, 20}")) path.nodes.extend([newNode1, newNode2]) self.assertEqual(newNode1, path.nodes[-2]) self.assertEqual(newNode2, path.nodes[-1]) del path.nodes[-2] del path.nodes[-1] self.assertEqual(amount, len(path.nodes)) # TODO: GSPath.closed # bezierPath? # TODO: # addNodesAtExtremes() # applyTransform() def test_direction(self): self.assertEqual(self.path.direction, -1) def test_segments(self): oldSegments = self.path.segments self.assertEqual(len(self.path.segments), 20) self.path.reverse() self.assertEqual(len(self.path.segments), 20) self.assertEqual(oldSegments[0].nodes[0], self.path.segments[0].nodes[0]) def test_bounds(self): bounds = self.path.bounds self.assertEqual(bounds.origin.x, 80) self.assertEqual(bounds.origin.y, -10) self.assertEqual(bounds.size.width, 289) self.assertEqual(bounds.size.height, 490) class GSNodeFromFileTest(GSObjectsTestCase): def setUp(self): super(GSNodeFromFileTest, self).setUp() self.glyph = self.font.glyphs["a"] self.layer = self.glyph.layers[0] self.path = self.layer.paths[0] self.node = self.path.nodes[0] def test_repr(self): self.assertIsNotNone(self.node.__repr__()) def test_position(self): self.assertIsInstance(self.node.position, point) def test_type(self): self.assertTrue(self.node.type in [GSNode.LINE, GSNode.CURVE, GSNode.OFFCURVE]) def test_smooth(self): self.assertBool(self.node.smooth) def test_index(self): self.assertInteger(self.node.index) self.assertEqual(self.path.nodes[0].index, 0) self.assertEqual(self.path.nodes[-1].index, 43) def test_nextNode(self): self.assertEqual(type(self.path.nodes[-1].nextNode), GSNode) self.assertEqual(self.path.nodes[-1].nextNode, self.path.nodes[0]) def test_prevNode(self): self.assertEqual(type(self.path.nodes[0].prevNode), GSNode) self.assertEqual(self.path.nodes[0].prevNode, self.path.nodes[-1]) def test_name(self): self.assertEqual(self.node.name, 'Hello') def test_userData(self): self.assertEqual("1", self.node.userData["rememberToMakeCoffee"]) def test_makeNodeFirst(self): oldAmount = len(self.path.nodes) oldSecondNode = self.path.nodes[3] self.path.nodes[3].makeNodeFirst() self.assertEqual(oldAmount, len(self.path.nodes)) self.assertEqual(oldSecondNode, self.path.nodes[0]) def test_toggleConnection(self): oldConnection = self.node.smooth self.node.toggleConnection() self.assertEqual(oldConnection, not self.node.smooth) class GSCustomParameterTest(unittest.TestCase): def test_plistValue_string(self): test_string = "Some Value" param = GSCustomParameter("New Parameter", test_string) self.assertEqual( param.plistValue(), '{\nname = "New Parameter";\nvalue = "Some Value";\n}' ) def test_plistValue_list(self): test_list = [ 1, 2.5, {"key1": "value1"}, ] param = GSCustomParameter("New Parameter", test_list) self.assertEqual( param.plistValue(), '{\nname = "New Parameter";\nvalue = (\n1,\n2.5,' '\n{\nkey1 = value1;\n}\n);\n}' ) def test_plistValue_dict(self): test_dict = { "key1": "value1", "key2": "value2", } param = GSCustomParameter("New Parameter", test_dict) self.assertEqual( param.plistValue(), '{\nname = "New Parameter";\nvalue = {\nkey1 = value1;' '\nkey2 = value2;\n};\n}' ) if __name__ == '__main__': unittest.main() glyphslib-2.2.1/tests/data/000077500000000000000000000000001322341616200155645ustar00rootroot00000000000000glyphslib-2.2.1/tests/data/DesignspaceTestBasic.designspace000066400000000000000000000054001322341616200240210ustar00rootroot00000000000000 Weight glyphslib-2.2.1/tests/data/DesignspaceTestFamilyName.designspace000066400000000000000000000036101322341616200250230ustar00rootroot00000000000000 Weight glyphslib-2.2.1/tests/data/DesignspaceTestFileName.designspace000066400000000000000000000036521322341616200244670ustar00rootroot00000000000000 Weight glyphslib-2.2.1/tests/data/DesignspaceTestInactive.designspace000066400000000000000000000027351322341616200245520ustar00rootroot00000000000000 Weight glyphslib-2.2.1/tests/data/DesignspaceTestInstanceOrder.designspace000066400000000000000000000046771322341616200255570ustar00rootroot00000000000000 Weight glyphslib-2.2.1/tests/data/DesignspaceTestTwoAxes.designspace000066400000000000000000000145511322341616200244010ustar00rootroot00000000000000 Weight Width glyphslib-2.2.1/tests/data/GlyphsUnitTestSans.glyphs000066400000000000000000001407571322341616200226250ustar00rootroot00000000000000{ .appVersion = "1087"; DisplayStrings = ( A ); classes = ( { code = A; name = c2sc_source; }, { code = a.sc; name = c2sc_target; }, { code = ""; name = numbers; }, { code = a; name = smcp_source; }, { code = a.sc; name = smcp_target; } ); customParameters = ( { name = note; value = "Bla bla"; } ); date = "2015-06-08 08:53:00 +0000"; familyName = "Glyphs Unit Test Sans"; featurePrefixes = ( { code = "# Dancing Shoes 0.1.4 OpenType feature code generator by Yanone, Copyright 2009\012# Code generated for AFDKO version 2.5\012\012\012languagesystem DFLT dflt; # Default, Default\012languagesystem latn dflt; # Latin, Default\012\012\012\012\012"; name = Languagesystems; } ); features = ( { automatic = 1; code = "feature c2sc;\012feature smcp;\012"; name = aalt; }, { code = "# Small Capitals From Capitals\012\012 sub @c2sc_source by @c2sc_target; "; name = c2sc; }, { code = "# Small Capitals\012\012 sub @smcp_source by @smcp_target; "; name = smcp; } ); fontMaster = ( { alignmentZones = ( "{800, 10}", "{700, 10}", "{470, 10}", "{0, -10}", "{-200, -10}" ); ascender = 800; capHeight = 700; customParameters = ( { name = TTFStems; value = ( { horizontal = 1; name = Thin; width = 16; }, { horizontal = 1; name = Lowercase; width = 16; }, { horizontal = 1; name = Uppercase; width = 18; } ); } ); descender = -200; guideLines = ( { position = "{-113, 574}"; }, { position = "{524, 141}"; }, { position = "{-113, 765}"; }, { position = "{524, -122}"; } ); horizontalStems = ( 16, 16, 18 ); iconName = Light; id = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; name = Light; userData = { GSOffsetHorizontal = 9; GSOffsetMakeStroke = 1; GSOffsetVertical = 9; GSRoughenHorizontal = 15; GSRoughenSegmentLength = 15; GSRoughenVertical = 10; }; verticalStems = ( 17, 19 ); weight = Light; weightValue = 17; xHeight = 470; }, { alignmentZones = ( "{800, 12}", "{700, 12}", "{480, 12}", "{0, -12}", "{-200, -12}" ); ascender = 800; capHeight = 700; customParameters = ( { name = TTFStems; value = ( { horizontal = 1; name = Thin; width = 80; }, { horizontal = 1; name = Lowercase; width = 88; }, { horizontal = 1; name = Uppercase; width = 91; } ); } ); descender = -200; guideLines = ( { position = "{-126, 593}"; }, { locked = 1; position = "{-126, 90}"; }, { position = "{-113, 773}"; }, { position = "{524, -133}"; }, { position = "{-126, 321}"; }, { position = "{-113, 959}"; } ); horizontalStems = ( 80, 88, 91 ); iconName = Regular; id = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; userData = { GSOffsetHorizontal = 45; GSOffsetMakeStroke = 1; GSOffsetVertical = 44; GSRoughenHorizontal = 15; GSRoughenSegmentLength = 15; GSRoughenVertical = 10; }; verticalStems = ( 90, 93 ); weightValue = 90; xHeight = 480; }, { alignmentZones = ( "{800, 15}", "{700, 15}", "{490, 15}", "{0, -15}", "{-200, -15}" ); ascender = 800; capHeight = 700; customParameters = ( { name = TTFStems; value = ( { horizontal = 1; name = Thin; width = 108; }, { horizontal = 1; name = Lowercase; width = 210; }, { horizontal = 1; name = Uppercase; width = 215; } ); } ); descender = -200; guideLines = ( { position = "{10, 660}"; }, { position = "{524, 44}"; }, { position = "{-113, 800}"; }, { position = "{524, -200}"; }, { position = "{998, -212}"; } ); horizontalStems = ( 108, 210, 215 ); iconName = Bold; id = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; name = Bold; userData = { GSOffsetHorizontal = 115; GSOffsetMakeStroke = 1; GSOffsetProportional = 1; GSOffsetVertical = 10; }; verticalStems = ( 220, 225 ); weight = Bold; weightValue = 220; xHeight = 490; } ); glyphs = ( { glyphname = A; lastChange = "2017-07-17 13:57:06 +0000"; layers = ( { anchors = ( { name = bottom; position = "{377, 0}"; }, { name = ogonek; position = "{678, 10}"; }, { name = top; position = "{377, 700}"; } ); background = { paths = ( { closed = 1; nodes = ( "566.99 700 LINE", "191 700 LINE", "24 0 LINE", "270 0 LINE", "364 470 LINE", "379 470 LINE", "477 0 LINE", "733 0 LINE" ); } ); }; layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; paths = ( { closed = 1; nodes = ( "555 700 LINE", "205 700 LINE", "20 0 LINE", "253 0 LINE", "356 470 LINE", "385 470 LINE", "491 0 LINE", "733 0 LINE" ); }, { closed = 1; nodes = ( "162 268 LINE", "154 103 LINE", "596 103 LINE", "600 268 LINE" ); } ); width = 753; }, { anchors = ( { name = bottom; position = "{329, 0}"; }, { name = ogonek; position = "{591, 10}"; }, { name = top; position = "{329, 700}"; } ); hints = ( { horizontal = 1; origin = "{1, 1}"; target = "{1, 0}"; }, { horizontal = 1; origin = "{0, 1}"; target = "{0, 4}"; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; paths = ( { closed = 1; nodes = ( "412 700 LINE", "248 700 LINE", "40 0 LINE", "134 0 LINE", "313 610 LINE", "342 610 LINE", "521 0 LINE", "617 0 LINE" ); }, { closed = 1; nodes = ( "150 269 LINE", "148 178 LINE", "510 178 LINE", "514 269 LINE" ); } ); width = 657; }, { anchors = ( { name = bottom; position = "{297, 0}"; }, { name = ogonek; position = "{548, 0}"; }, { name = top; position = "{297, 700}"; } ); background = { paths = ( { closed = 1; nodes = ( "133 253 LINE", "134 215 LINE", "451 215 LINE", "455 253 LINE" ); } ); }; backgroundImage = { crop = "{{0, 0}, {489, 637}}"; imagePath = A.jpg; }; guideLines = ( { angle = 71.7587; position = "{45, 0}"; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; paths = ( { closed = 1; nodes = ( "321 700 LINE {name = \"Hello World\";}", "275 700 LINE", "45 0 LINE", "65 0 LINE", "289 679 LINE", "307 679 LINE", "527 0 LINE", "548 0 LINE" ); }, { closed = 1; nodes = ( "128 225 LINE", "123 207 LINE", "472 207 LINE", "467 225 LINE" ); } ); width = 593; } ); leftKerningGroup = A; rightKerningGroup = A; unicode = 0041; script = ""; category = ""; subCategory = ""; }, { glyphname = Adieresis; layers = ( { components = ( { name = A; }, { name = dieresis; transform = "{1, 0, 0, 1, 110, 230}"; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; width = 593; }, { components = ( { name = A; }, { name = dieresis; transform = "{1, 0, 0, 1, 128, 220}"; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; width = 657; }, { components = ( { name = A; }, { name = dieresis; transform = "{1, 0, 0, 1, 110, 210}"; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; width = 753; } ); unicode = 00C4; }, { glyphname = a; lastChange = "2017-07-17 13:49:10 +0000"; layers = ( { anchors = ( { name = bottom; position = "{184, 0}"; }, { name = ogonek; position = "{488, 0}"; }, { name = top; position = "{258, 490}"; } ); background = { paths = ( { closed = 1; nodes = ( "268 153 LINE SMOOTH", "268 123 OFFCURVE", "254 113 OFFCURVE", "236 113 CURVE SMOOTH", "214 113 OFFCURVE", "205 127 OFFCURVE", "205 143 CURVE SMOOTH", "205 155 OFFCURVE", "210 164 OFFCURVE", "218 170 CURVE SMOOTH", "233 181 OFFCURVE", "254 182 OFFCURVE", "275 182 CURVE", "295 289 LINE", "203 289 OFFCURVE", "123 277 OFFCURVE", "72 240 CURVE SMOOTH", "40 216 OFFCURVE", "21 182 OFFCURVE", "21 133 CURVE SMOOTH", "21 49 OFFCURVE", "75 -8 OFFCURVE", "184 -8 CURVE SMOOTH", "260 -8 OFFCURVE", "301 16 OFFCURVE", "322 49 CURVE", "309 57 LINE", "336 0 LINE", "488 0 LINE", "488 454 LINE", "437 484 OFFCURVE", "354 505 OFFCURVE", "255 505 CURVE SMOOTH", "167 505 OFFCURVE", "86 489 OFFCURVE", "25 461 CURVE", "56 298 LINE", "90 311 OFFCURVE", "134 322 OFFCURVE", "194 322 CURVE SMOOTH", "225 322 OFFCURVE", "270 319 OFFCURVE", "308 305 CURVE", "268 392 LINE" ); } ); }; layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; paths = ( { closed = 1; nodes = ( "268 153 LINE SMOOTH", "268 123 OFFCURVE", "254 113 OFFCURVE", "236 113 CURVE SMOOTH", "214 113 OFFCURVE", "205 127 OFFCURVE", "205 143 CURVE SMOOTH", "205 155 OFFCURVE", "210 164 OFFCURVE", "218 170 CURVE SMOOTH", "233 181 OFFCURVE", "254 182 OFFCURVE", "275 182 CURVE", "295 289 LINE", "203 289 OFFCURVE", "123 277 OFFCURVE", "72 240 CURVE SMOOTH", "40 216 OFFCURVE", "21 182 OFFCURVE", "21 133 CURVE SMOOTH", "21 49 OFFCURVE", "75 -8 OFFCURVE", "184 -8 CURVE SMOOTH", "250 -8 OFFCURVE", "288 12 OFFCURVE", "310 44 CURVE", "320 44 LINE", "336 0 LINE", "488 0 LINE", "488 454 LINE", "437 484 OFFCURVE", "354 505 OFFCURVE", "255 505 CURVE SMOOTH", "167 505 OFFCURVE", "86 489 OFFCURVE", "25 461 CURVE", "56 298 LINE", "90 311 OFFCURVE", "134 322 OFFCURVE", "194 322 CURVE SMOOTH", "225 322 OFFCURVE", "270 319 OFFCURVE", "308 305 CURVE", "268 392 LINE" ); } ); width = 518; }, { anchors = ( { name = bottom; position = "{218, 0}"; }, { name = ogonek; position = "{423, 0}"; }, { name = top; position = "{248, 480}"; } ); annotations = ( { position = "{427, 535}"; text = "This is a text annotation"; type = Text; }, { angle = 41.22902; position = "{436, 446}"; type = Arrow; }, { position = "{334.937, 407.08}"; type = Circle; width = 65.05341; }, { position = "{301, 49}"; type = Plus; }, { position = "{372, 172}"; type = Minus; } ); background = { paths = ( { closed = 1; nodes = ( "333 176 LINE SMOOTH", "333 119 OFFCURVE", "309 69 OFFCURVE", "231 69 CURVE SMOOTH", "170 69 OFFCURVE", "153 99 OFFCURVE", "153 127 CURVE SMOOTH", "153 152 OFFCURVE", "166 169 OFFCURVE", "183 179 CURVE SMOOTH", "219 201 OFFCURVE", "284 204 OFFCURVE", "338 204 CURVE", "338 282 LINE", "249 282 OFFCURVE", "193 276 OFFCURVE", "142 251 CURVE SMOOTH", "94 227 OFFCURVE", "65 184 OFFCURVE", "65 124 CURVE SMOOTH", "65 41 OFFCURVE", "119 -11 OFFCURVE", "215 -11 CURVE SMOOTH", "283 -11 OFFCURVE", "325 14 OFFCURVE", "352 56 CURVE", "330 67 LINE", "346 0 LINE", "423 0 LINE", "423 435 LINE", "383 468 OFFCURVE", "317 492 OFFCURVE", "237 492 CURVE SMOOTH", "172 492 OFFCURVE", "117 476 OFFCURVE", "72 460 CURVE", "86 371 LINE", "122 387 OFFCURVE", "167 400 OFFCURVE", "226 400 CURVE SMOOTH", "263 400 OFFCURVE", "312 395 OFFCURVE", "353 361 CURVE", "333 454 LINE" ); } ); }; layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; paths = ( { closed = 1; nodes = ( "333 176 LINE SMOOTH", "333 119 OFFCURVE", "309 69 OFFCURVE", "231 69 CURVE SMOOTH", "170 69 OFFCURVE", "153 99 OFFCURVE", "153 127 CURVE SMOOTH", "153 152 OFFCURVE", "166 169 OFFCURVE", "183 179 CURVE SMOOTH", "219 200 OFFCURVE", "284 204 OFFCURVE", "338 204 CURVE", "338 282 LINE", "249 282 OFFCURVE", "193 276 OFFCURVE", "142 251 CURVE SMOOTH", "94 227 OFFCURVE", "65 185 OFFCURVE", "65 125 CURVE SMOOTH", "65 42 OFFCURVE", "119 -11 OFFCURVE", "215 -11 CURVE SMOOTH", "277 -11 OFFCURVE", "310 11 OFFCURVE", "330 41 CURVE", "338 41 LINE", "346 0 LINE", "423 0 LINE", "423 435 LINE", "383 468 OFFCURVE", "316 492 OFFCURVE", "232 492 CURVE SMOOTH", "171 492 OFFCURVE", "116 479 OFFCURVE", "72 460 CURVE", "86 371 LINE", "122 388 OFFCURVE", "166 400 OFFCURVE", "225 400 CURVE SMOOTH", "262 400 OFFCURVE", "312 395 OFFCURVE", "353 361 CURVE", "333 454 LINE" ); } ); width = 496; }, { anchors = ( { name = bottom; position = "{218, 0}"; }, { name = ogonek; position = "{369, 0}"; }, { name = top; position = "{226, 471}"; } ); background = { paths = ( { closed = 1; nodes = ( "352 147 LINE SMOOTH", "352 68 OFFCURVE", "314 7 OFFCURVE", "212 7 CURVE SMOOTH", "132 7 OFFCURVE", "97 47 OFFCURVE", "97 105 CURVE SMOOTH", "97 136 OFFCURVE", "107 160 OFFCURVE", "125 178 CURVE SMOOTH", "166 219 OFFCURVE", "249 224 OFFCURVE", "355 224 CURVE", "355 241 LINE", "245 241 OFFCURVE", "158 233 OFFCURVE", "113 190 CURVE SMOOTH", "92 169 OFFCURVE", "80 141 OFFCURVE", "80 105 CURVE SMOOTH", "80 39 OFFCURVE", "119 -10 OFFCURVE", "212 -10 CURVE SMOOTH", "281 -10 OFFCURVE", "336 15 OFFCURVE", "357 83 CURVE", "351 91 LINE", "354 0 LINE", "369 0 LINE", "369 428 LINE", "333 460 OFFCURVE", "291 480 OFFCURVE", "224 480 CURVE SMOOTH", "163 480 OFFCURVE", "116 462 OFFCURVE", "82 438 CURVE", "87 423 LINE", "123 448 OFFCURVE", "167 463 OFFCURVE", "224 463 CURVE SMOOTH", "284 463 OFFCURVE", "323 445 OFFCURVE", "355 417 CURVE", "352 429 LINE" ); } ); }; layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; paths = ( { closed = 1; nodes = ( "352 147 LINE SMOOTH {name = Hello;\nrememberToMakeCoffee = \"1\";}", "352 68 OFFCURVE", "314 7 OFFCURVE", "212 7 CURVE SMOOTH", "132 7 OFFCURVE", "97 47 OFFCURVE", "97 105 CURVE SMOOTH", "97 136 OFFCURVE", "107 160 OFFCURVE", "125 178 CURVE SMOOTH", "166 219 OFFCURVE", "249 224 OFFCURVE", "355 224 CURVE", "355 241 LINE", "245 241 OFFCURVE", "158 233 OFFCURVE", "113 190 CURVE SMOOTH", "92 169 OFFCURVE", "80 141 OFFCURVE", "80 105 CURVE SMOOTH", "80 39 OFFCURVE", "119 -10 OFFCURVE", "212 -10 CURVE SMOOTH", "283 -10 OFFCURVE", "334 18 OFFCURVE", "349 68 CURVE", "352 68 LINE", "354 0 LINE", "369 0 LINE", "369 428 LINE", "333 460 OFFCURVE", "291 480 OFFCURVE", "224 480 CURVE SMOOTH", "163 480 OFFCURVE", "116 462 OFFCURVE", "82 438 CURVE", "87 423 LINE", "123 448 OFFCURVE", "167 463 OFFCURVE", "224 463 CURVE SMOOTH", "284 463 OFFCURVE", "323 445 OFFCURVE", "355 417 CURVE", "352 429 LINE" ); } ); width = 456; }, { anchors = ( { name = bottom; position = "{189, 0}"; }, { name = ogonek; position = "{446, 0}"; }, { name = top; position = "{237, 485}"; } ); associatedMasterId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; background = { paths = ( { closed = 1; nodes = ( "291 164 LINE SMOOTH", "291 129 OFFCURVE", "272 107 OFFCURVE", "224 107 CURVE SMOOTH", "182 107 OFFCURVE", "169 123 OFFCURVE", "169 140 CURVE SMOOTH", "169 155 OFFCURVE", "178 165 OFFCURVE", "191 172 CURVE SMOOTH", "216 185 OFFCURVE", "259 186 OFFCURVE", "297 186 CURVE", "306 298 LINE", "216 294 OFFCURVE", "148 288 OFFCURVE", "97 255 CURVE SMOOTH", "58 230 OFFCURVE", "34 187 OFFCURVE", "34 130 CURVE SMOOTH", "34 40 OFFCURVE", "87 -10 OFFCURVE", "190 -10 CURVE SMOOTH", "262 -10 OFFCURVE", "303 15 OFFCURVE", "327 53 CURVE", "310 62 LINE", "331 0 LINE", "446 0 LINE", "446 445 LINE", "400 476 OFFCURVE", "326 499 OFFCURVE", "236 499 CURVE SMOOTH", "160 499 OFFCURVE", "92 483 OFFCURVE", "39 461 CURVE", "61 329 LINE", "96 343 OFFCURVE", "141 355 OFFCURVE", "200 355 CURVE SMOOTH", "234 355 OFFCURVE", "281 351 OFFCURVE", "321 327 CURVE", "291 423 LINE" ); } ); }; layerId = "1FA54028-AD2E-4209-AA7B-72DF2DF16264"; name = "{155, 100}"; paths = ( { closed = 1; nodes = ( "301 174 LINE SMOOTH", "301 129 OFFCURVE", "272 107 OFFCURVE", "224 107 CURVE SMOOTH", "182 107 OFFCURVE", "169 123 OFFCURVE", "169 140 CURVE SMOOTH", "169 155 OFFCURVE", "178 165 OFFCURVE", "191 172 CURVE SMOOTH", "216 185 OFFCURVE", "259 186 OFFCURVE", "307 186 CURVE", "306 298 LINE", "216 294 OFFCURVE", "148 288 OFFCURVE", "97 255 CURVE SMOOTH", "58 230 OFFCURVE", "34 190 OFFCURVE", "34 132 CURVE SMOOTH", "34 47 OFFCURVE", "81 -10 OFFCURVE", "190 -10 CURVE SMOOTH", "252 -10 OFFCURVE", "297 8 OFFCURVE", "320 42 CURVE", "329 42 LINE", "341 0 LINE", "446 0 LINE", "446 445 LINE", "400 476 OFFCURVE", "326 499 OFFCURVE", "236 499 CURVE SMOOTH", "160 499 OFFCURVE", "92 483 OFFCURVE", "39 461 CURVE", "61 329 LINE", "96 343 OFFCURVE", "141 355 OFFCURVE", "200 355 CURVE SMOOTH", "234 355 OFFCURVE", "281 351 OFFCURVE", "321 327 CURVE", "301 423 LINE" ); } ); width = 496; } ); leftKerningGroup = a; rightKerningGroup = a; rightMetricsKey = m; unicode = 0061; }, { glyphname = adieresis; lastChange = "2016-03-18 09:51:27 +0000"; layers = ( { components = ( { name = a; }, { name = dieresis; transform = "{1, 0, 0, 1, 39, 1}"; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; width = 456; }, { components = ( { name = a; }, { name = dieresis; transform = "{1, 0, 0, 1, 47, 0}"; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; width = 496; }, { components = ( { name = a; }, { name = dieresis; transform = "{1, 0, 0, 1, -9, 0}"; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; width = 518; } ); unicode = 00E4; }, { glyphname = h; lastChange = "2017-10-09 14:18:19 +0000"; layers = ( { components = ( { name = _part.stem; piece = { height = 100; }; }, { name = _part.shoulder; piece = { crotchDepth = -80.20097; }; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; width = 511; }, { components = ( { name = _part.stem; piece = { height = 100; }; }, { name = _part.shoulder; piece = { crotchDepth = -80.20097; }; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; width = 532; }, { components = ( { name = _part.stem; piece = { height = 100; }; }, { name = _part.shoulder; piece = { crotchDepth = -80.20097; }; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; width = 560; } ); leftMetricsKey = m; rightMetricsKey = m; unicode = 0068; }, { glyphname = m; lastChange = "2017-10-09 14:03:19 +0000"; layers = ( { components = ( { name = _part.stem; }, { name = _part.shoulder; piece = { shoulderWidth = 0; }; }, { name = _part.shoulder; piece = { shoulderWidth = 0; }; transform = "{1, 0, 0, 1, 264, 0}"; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; width = 745; }, { components = ( { name = _part.stem; }, { name = _part.shoulder; piece = { shoulderWidth = 0; }; }, { name = _part.shoulder; piece = { shoulderWidth = 0; }; transform = "{1, 0, 0, 1, 270, 0}"; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; width = 820; }, { components = ( { name = _part.stem; }, { name = _part.shoulder; piece = { shoulderWidth = 0; }; }, { name = _part.shoulder; piece = { shoulderWidth = 0; }; transform = "{1, 0, 0, 1, 258, 0}"; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; width = 760; } ); unicode = 006D; }, { glyphname = n; lastChange = "2017-10-09 14:04:04 +0000"; layers = ( { background = { paths = ( { closed = 1; nodes = ( "250 0 LINE", "250 240 LINE SMOOTH", "250 272 OFFCURVE", "264 283 OFFCURVE", "284 283 CURVE SMOOTH", "295 283 OFFCURVE", "304 280 OFFCURVE", "310 276 CURVE", "310 0 LINE", "530 0 LINE", "530 448 LINE", "490 478 OFFCURVE", "430 501 OFFCURVE", "357 501 CURVE SMOOTH", "256 501 OFFCURVE", "199 459 OFFCURVE", "173 386 CURVE", "197 366 LINE", "162 490 LINE", "30 490 LINE", "30 0 LINE" ); } ); }; components = ( { name = _part.shoulder; }, { name = _part.stem; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; width = 560; }, { background = { paths = ( { closed = 1; nodes = ( "167 0 LINE", "167 270 LINE SMOOTH", "167 362 OFFCURVE", "209 402 OFFCURVE", "277 402 CURVE SMOOTH", "315 402 OFFCURVE", "345 390 OFFCURVE", "365 370 CURVE", "365 0 LINE", "455 0 LINE", "455 423 LINE", "423 454 OFFCURVE", "374 490 OFFCURVE", "288 490 CURVE SMOOTH", "199 490 OFFCURVE", "150 452 OFFCURVE", "139 382 CURVE", "159 356 LINE", "139 480 LINE", "77 480 LINE", "77 0 LINE" ); } ); }; components = ( { name = _part.shoulder; }, { name = _part.stem; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; width = 528; }, { background = { paths = ( { closed = 1; nodes = ( "117 0 LINE", "117 322 LINE SMOOTH", "117 435 OFFCURVE", "175 491 OFFCURVE", "270 491 CURVE SMOOTH", "324 491 OFFCURVE", "366 474 OFFCURVE", "394 445 CURVE", "394 -1 LINE", "411 -1 LINE", "411 435 LINE", "378 475 OFFCURVE", "340 509 OFFCURVE", "263 509 CURVE SMOOTH", "180 509 OFFCURVE", "133 468 OFFCURVE", "117 406 CURVE", "134 376 LINE", "123 500 LINE", "100 500 LINE", "100 0 LINE" ); } ); }; components = ( { name = _part.shoulder; }, { name = _part.stem; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; width = 501; } ); leftKerningGroup = n; leftMetricsKey = m; rightKerningGroup = n; rightMetricsKey = m; unicode = 006E; }, { color = 10; glyphname = a.sc; lastChange = "2016-12-19 17:35:01 +0000"; layers = ( { anchors = ( { name = bottom; position = "{307, 0}"; }, { name = ogonek; position = "{608, 0}"; }, { name = top; position = "{307, 552}"; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; paths = ( { closed = 1; nodes = ( "461 552 LINE", "158 552 LINE", "5 0 LINE", "208 0 LINE", "289 351 LINE", "312 351 LINE", "395 0 LINE", "608 0 LINE" ); }, { closed = 1; nodes = ( "122 234 LINE", "115 65 LINE", "495 65 LINE", "498 234 LINE" ); } ); width = 613; }, { anchors = ( { name = bottom; position = "{268, 0}"; }, { name = ogonek; position = "{511, 0}"; }, { name = top; position = "{268, 540}"; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; paths = ( { closed = 1; nodes = ( "357 540 LINE", "191 540 LINE", "24 0 LINE", "119 0 LINE", "255 448 LINE", "277 448 LINE", "414 0 LINE", "511 0 LINE" ); }, { closed = 1; nodes = ( "114 208 LINE", "111 125 LINE", "424 125 LINE", "427 208 LINE" ); } ); width = 535; }, { anchors = ( { name = bottom; position = "{240, 0}"; }, { name = ogonek; position = "{445, 0}"; }, { name = top; position = "{240, 528}"; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; paths = ( { closed = 1; nodes = ( "252 528 LINE", "228 528 LINE", "33 0 LINE", "41 0 LINE", "234 519 LINE", "247 519 LINE", "437 0 LINE", "445 0 LINE" ); }, { closed = 1; nodes = ( "99 167 LINE", "95 159 LINE", "384 159 LINE", "380 167 LINE" ); } ); width = 478; } ); leftKerningGroup = A.sc; rightKerningGroup = A.sc; rightMetricsKey = "=|"; }, { glyphname = dieresis; lastChange = "2015-12-31 14:44:23 +0000"; layers = ( { anchors = ( { name = _top; position = "{187, 470}"; }, { name = top; position = "{188, 650}"; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; paths = ( { closed = 1; nodes = ( "261 650 LINE", "261 621 LINE", "289 621 LINE", "289 650 LINE" ); }, { closed = 1; nodes = ( "88 650 LINE", "88 621 LINE", "116 621 LINE", "116 650 LINE" ); } ); width = 600; }, { anchors = ( { name = _top; position = "{201, 480}"; }, { name = top; position = "{201, 700}"; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; paths = ( { closed = 1; nodes = ( "252 700 LINE", "252 601 LINE", "349 601 LINE", "349 700 LINE" ); }, { closed = 1; nodes = ( "52 700 LINE", "52 601 LINE", "149 601 LINE", "149 700 LINE" ); } ); width = 600; }, { anchors = ( { name = _top; position = "{267, 490}"; }, { name = top; position = "{267, 740}"; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; paths = ( { closed = 1; nodes = ( "298 735 LINE", "298 547 LINE", "482 547 LINE", "482 735 LINE" ); }, { closed = 1; nodes = ( "48 735 LINE", "48 547 LINE", "232 547 LINE", "232 735 LINE" ); } ); width = 600; } ); unicode = 00A8; }, { export = 0; glyphname = _part.shoulder; lastChange = "2017-10-09 13:47:55 +0000"; layers = ( { anchors = ( { name = _connect; position = "{117, 0}"; }, { name = connect; position = "{411, 0}"; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; paths = ( { closed = 1; nodes = ( "117 266 LINE", "117 410 OFFCURVE", "173 463 OFFCURVE", "262 463 CURVE SMOOTH", "322 463 OFFCURVE", "365 437 OFFCURVE", "394 408 CURVE", "394 0 LINE", "411 0 LINE", "411 414 LINE", "377 450 OFFCURVE", "333 479 OFFCURVE", "262 479 CURVE SMOOTH", "169 479 OFFCURVE", "132 429 OFFCURVE", "121 384 CURVE", "102 384 LINE" ); } ); userData = { PartSelection = { crotchDepth = 2; shoulderWidth = 2; }; }; width = 501; }, { anchors = ( { name = _connect; position = "{167, 0}"; }, { name = connect; position = "{455, 0}"; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; paths = ( { closed = 1; nodes = ( "167 246 LINE", "167 355 OFFCURVE", "203 402 OFFCURVE", "277 402 CURVE SMOOTH", "315 402 OFFCURVE", "345 390 OFFCURVE", "365 370 CURVE", "365 0 LINE", "455 0 LINE", "455 423 LINE", "425 452 OFFCURVE", "380 490 OFFCURVE", "294 490 CURVE SMOOTH", "205 490 OFFCURVE", "170 452 OFFCURVE", "155 409 CURVE", "131 409 LINE" ); } ); userData = { PartSelection = { crotchDepth = 2; shoulderWidth = 2; }; }; width = 528; }, { anchors = ( { name = _connect; position = "{250, 0}"; }, { name = connect; position = "{530, 0}"; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; paths = ( { closed = 1; nodes = ( "250 229 LINE", "250 268 OFFCURVE", "259 283 OFFCURVE", "284 283 CURVE SMOOTH", "295 283 OFFCURVE", "304 280 OFFCURVE", "310 276 CURVE", "310 0 LINE", "530 0 LINE", "530 448 LINE", "490 478 OFFCURVE", "430 501 OFFCURVE", "357 501 CURVE SMOOTH", "259 501 OFFCURVE", "209 461 OFFCURVE", "188 401 CURVE", "162 401 LINE" ); } ); userData = { PartSelection = { crotchDepth = 2; shoulderWidth = 2; }; }; width = 560; }, { anchors = ( { name = _connect; position = "{117, 0}"; }, { name = connect; position = "{381, 0}"; } ); associatedMasterId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; layerId = "50EFFDD5-7E17-4ADC-BBC9-E16AAD6631DE"; name = NarrowShoulder; paths = ( { closed = 1; nodes = ( "117 266 LINE", "117 410 OFFCURVE", "173 463 OFFCURVE", "252 463 CURVE SMOOTH", "292 463 OFFCURVE", "335 437 OFFCURVE", "364 408 CURVE", "364 0 LINE", "381 0 LINE", "381 414 LINE", "347 450 OFFCURVE", "303 479 OFFCURVE", "252 479 CURVE SMOOTH", "169 479 OFFCURVE", "132 429 OFFCURVE", "121 384 CURVE", "102 384 LINE" ); } ); userData = { PartSelection = { crotchDepth = 2; shoulderWidth = 1; }; }; width = 501; }, { anchors = ( { name = _connect; position = "{117, 0}"; }, { name = connect; position = "{411, 0}"; } ); associatedMasterId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; layerId = "7C8F98EE-D140-44D5-86AE-E00A730464C0"; name = LowCrotch; paths = ( { closed = 1; nodes = ( "117 236 LINE", "117 410 OFFCURVE", "173 463 OFFCURVE", "262 463 CURVE SMOOTH", "322 463 OFFCURVE", "365 437 OFFCURVE", "394 408 CURVE", "394 0 LINE", "411 0 LINE", "411 414 LINE", "377 450 OFFCURVE", "333 479 OFFCURVE", "262 479 CURVE SMOOTH", "169 479 OFFCURVE", "132 429 OFFCURVE", "121 354 CURVE", "102 354 LINE" ); } ); userData = { PartSelection = { crotchDepth = 1; shoulderWidth = 2; }; }; width = 501; }, { anchors = ( { name = _connect; position = "{167, 0}"; }, { name = connect; position = "{425, 0}"; } ); associatedMasterId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; layerId = "595FDB8C-ED41-486A-B76A-0FEFEF8BCDD1"; name = NarrowShoulder; paths = ( { closed = 1; nodes = ( "167 246 LINE", "167 355 OFFCURVE", "203 402 OFFCURVE", "257 402 CURVE SMOOTH", "285 402 OFFCURVE", "315 390 OFFCURVE", "335 370 CURVE", "335 0 LINE", "425 0 LINE", "425 423 LINE", "395 452 OFFCURVE", "350 490 OFFCURVE", "274 490 CURVE SMOOTH", "205 490 OFFCURVE", "170 452 OFFCURVE", "155 409 CURVE", "131 409 LINE" ); } ); userData = { PartSelection = { crotchDepth = 2; shoulderWidth = 1; }; }; width = 528; }, { anchors = ( { name = _connect; position = "{167, 0}"; }, { name = connect; position = "{455, 0}"; } ); associatedMasterId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; layerId = "65575EEB-523C-4A39-985D-FB9ACFE951AF"; name = LowCrotch; paths = ( { closed = 1; nodes = ( "167 216 LINE", "167 325 OFFCURVE", "203 402 OFFCURVE", "277 402 CURVE SMOOTH", "315 402 OFFCURVE", "345 390 OFFCURVE", "365 370 CURVE", "365 0 LINE", "455 0 LINE", "455 423 LINE", "425 452 OFFCURVE", "380 490 OFFCURVE", "294 490 CURVE SMOOTH", "205 490 OFFCURVE", "170 452 OFFCURVE", "155 379 CURVE", "131 379 LINE" ); } ); userData = { PartSelection = { crotchDepth = 1; shoulderWidth = 2; }; }; width = 528; }, { anchors = ( { name = _connect; position = "{250, 0}"; }, { name = connect; position = "{520, 0}"; } ); associatedMasterId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; layerId = "D607B100-382C-478B-A297-2EF174C3A363"; name = NarrowShoulder; paths = ( { closed = 1; nodes = ( "250 229 LINE", "250 268 OFFCURVE", "259 283 OFFCURVE", "274 283 CURVE SMOOTH", "285 283 OFFCURVE", "294 280 OFFCURVE", "300 276 CURVE", "300 0 LINE", "520 0 LINE", "520 448 LINE", "480 478 OFFCURVE", "420 501 OFFCURVE", "347 501 CURVE SMOOTH", "259 501 OFFCURVE", "209 461 OFFCURVE", "188 401 CURVE", "162 401 LINE" ); } ); userData = { PartSelection = { crotchDepth = 2; shoulderWidth = 1; }; }; width = 560; }, { anchors = ( { name = _connect; position = "{250, 0}"; }, { name = connect; position = "{530, 0}"; } ); associatedMasterId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; layerId = "BA4F7DF9-9552-48BB-A5B8-E2D21D8D086E"; name = LowCrotch; paths = ( { closed = 1; nodes = ( "250 199 LINE", "250 251 OFFCURVE", "259 283 OFFCURVE", "284 283 CURVE SMOOTH", "295 283 OFFCURVE", "304 280 OFFCURVE", "310 276 CURVE", "310 0 LINE", "530 0 LINE", "530 448 LINE", "490 478 OFFCURVE", "430 501 OFFCURVE", "357 501 CURVE SMOOTH", "259 501 OFFCURVE", "209 461 OFFCURVE", "188 371 CURVE", "162 371 LINE" ); } ); userData = { PartSelection = { crotchDepth = 1; shoulderWidth = 2; }; }; width = 560; } ); partsSettings = ( { name = crotchDepth; bottomName = Low; bottomValue = -100; topName = High; topValue = 0; }, { name = shoulderWidth; bottomName = Low; bottomValue = 0; topName = High; topValue = 100; } ); }, { export = 0; glyphname = _part.stem; lastChange = "2017-10-09 14:18:06 +0000"; layers = ( { anchors = ( { name = connect; position = "{117, 0}"; } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; paths = ( { closed = 1; nodes = ( "119 368 LINE", "115 470 LINE", "100 470 LINE", "100 0 LINE", "117 0 LINE", "117 306 LINE" ); } ); userData = { PartSelection = { height = 1; }; }; width = 600; }, { anchors = ( { name = connect; position = "{167, 0}"; } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; paths = ( { closed = 1; nodes = ( "149 393 LINE", "139 480 LINE", "77 480 LINE", "77 0 LINE", "167 0 LINE", "167 286 LINE" ); } ); userData = { PartSelection = { height = 1; }; }; width = 600; }, { anchors = ( { name = connect; position = "{250, 0}"; } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; paths = ( { closed = 1; nodes = ( "181 385 LINE", "162 490 LINE", "30 490 LINE", "30 0 LINE", "250 0 LINE", "250 256 LINE" ); } ); userData = { PartSelection = { height = 1; }; }; width = 600; }, { anchors = ( { name = connect; position = "{117, 0}"; } ); associatedMasterId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; layerId = "0D68D3E9-A0B2-4D78-A161-EB65D8511F0A"; name = TallStem; paths = ( { closed = 1; nodes = ( "117 368 LINE", "117 800 LINE", "100 800 LINE", "100 0 LINE", "117 0 LINE", "117 306 LINE" ); } ); userData = { PartSelection = { height = 2; }; }; width = 600; }, { anchors = ( { name = connect; position = "{167, 0}"; } ); associatedMasterId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; layerId = "3E1733D9-3B83-4E6A-B1E9-6381BBE1BD3A"; name = TallStem; paths = ( { closed = 1; nodes = ( "167 393 LINE", "167 800 LINE", "77 800 LINE", "77 0 LINE", "167 0 LINE", "167 286 LINE" ); } ); userData = { PartSelection = { height = 2; }; }; width = 600; }, { anchors = ( { name = connect; position = "{250, 0}"; } ); associatedMasterId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; layerId = "FD65D427-9013-43E2-9F74-398D99AA4763"; name = TallStem; paths = ( { closed = 1; nodes = ( "250 385 LINE", "250 800 LINE", "30 800 LINE", "30 0 LINE", "250 0 LINE", "250 256 LINE" ); } ); userData = { PartSelection = { height = 2; }; com.typemytype.robofont.layerData = { "Regular Nov 6 15, 18:44" = { anchors = ( ); components = ( { baseGlyph = tmA; transformation = ( 1, 0, 0, 1, 0, 0 ); } ); contours = ( { points = ( { smooth = 0; x = 2570; y = -907; } ); } ); lib = { }; name = tmAA; unicodes = ( ); width = 3047; }; }; }; width = 600; } ); partsSettings = ( { name = height; bottomName = Low; bottomValue = 0; topName = High; topValue = 100; } ); } ); instances = ( { interpolationWeight = 17; instanceInterpolations = { "C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = 1; }; manualInterpolation = 1; name = Thin; weightClass = Thin; }, { interpolationWeight = 30; instanceInterpolations = { "3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.17808; "C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = 0.82192; }; name = "Extra Light"; weightClass = ExtraLight; }, { interpolationWeight = 55; instanceInterpolations = { "3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.52055; "C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = 0.47945; }; name = Light; weightClass = Light; }, { interpolationWeight = 90; instanceInterpolations = { "3E7589AA-8194-470F-8E2F-13C1C581BE24" = 1; }; name = Regular; }, { interpolationWeight = 133; instanceInterpolations = { "3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.66923; "BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = 0.33077; }; name = Medium; weightClass = Medium; }, { interpolationWeight = 179; instanceInterpolations = { "3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.31538; "BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = 0.68462; }; name = Bold; weightClass = Bold; }, { interpolationWeight = 220; instanceInterpolations = { "BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = 1; }; name = Black; weightClass = Black; }, { interpolationWeight = 75; instanceInterpolations = { "3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.79452; "C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = 0.20548; }; name = Web; } ); kerning = { "C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = { "@MMK_L_A" = { "@MMK_R_J" = -30; "@MMK_R_O" = -20; "@MMK_R_T" = -100; "@MMK_R_U" = -20; "@MMK_R_V" = -10; "@MMK_R_Y" = -40; "@MMK_R_o" = -10; "@MMK_R_quote" = -60; "@MMK_R_quoteright" = -60; "@MMK_R_t" = -30; "@MMK_R_u" = -20; "@MMK_R_v" = -20; "@MMK_R_w" = -20; "@MMK_R_y" = -20; }; "@MMK_L_B" = { "@MMK_R_T" = -50; "@MMK_R_V" = -30; "@MMK_R_Y" = -40; "@MMK_R_v" = -10; }; "@MMK_L_C" = { "@MMK_R_O" = -20; "@MMK_R_o" = -10; }; "@MMK_L_E" = { "@MMK_R_O" = -20; "@MMK_R_V" = -20; "@MMK_R_Y" = -20; }; "@MMK_L_F" = { "@MMK_R_A" = -60; "@MMK_R_O" = -30; "@MMK_R_a" = -50; "@MMK_R_o" = -50; }; "@MMK_L_K" = { "@MMK_R_O" = -30; "@MMK_R_a" = -30; "@MMK_R_o" = -30; "@MMK_R_v" = -30; "@MMK_R_y" = -20; }; "@MMK_L_L" = { "@MMK_R_O" = -70; "@MMK_R_T" = -110; "@MMK_R_U" = -20; "@MMK_R_V" = -90; "@MMK_R_Y" = -110; "@MMK_R_a" = -20; "@MMK_R_o" = -20; "@MMK_R_quote" = -80; "@MMK_R_quoteright" = -80; "@MMK_R_t" = -40; "@MMK_R_u" = -20; "@MMK_R_v" = -70; }; "@MMK_L_M" = { "@MMK_R_O" = -20; "@MMK_R_T" = -40; "@MMK_R_V" = -20; "@MMK_R_Y" = -30; "@MMK_R_o" = -10; "@MMK_R_v" = -20; }; "@MMK_L_O" = { "@MMK_R_A" = -20; "@MMK_R_J" = -30; "@MMK_R_M" = -20; "@MMK_R_T" = -60; "@MMK_R_V" = -10; "@MMK_R_X" = -30; "@MMK_R_Y" = -20; "@MMK_R_Z" = -40; "@MMK_R_quoteright" = -20; }; "@MMK_L_P" = { "@MMK_R_A" = -40; "@MMK_R_V" = -10; "@MMK_R_Y" = -20; "@MMK_R_a" = -20; "@MMK_R_o" = -20; "@MMK_R_s" = -10; }; "@MMK_L_R" = { "@MMK_R_O" = -20; "@MMK_R_T" = -30; "@MMK_R_V" = -20; "@MMK_R_Y" = -30; "@MMK_R_a" = -30; "@MMK_R_o" = -30; "@MMK_R_u" = -10; "@MMK_R_v" = -20; "@MMK_R_y" = -20; }; "@MMK_L_S" = { "@MMK_R_T" = -20; "@MMK_R_V" = -20; }; "@MMK_L_T" = { "@MMK_R_A" = -100; "@MMK_R_M" = -50; "@MMK_R_O" = -60; "@MMK_R_S" = -40; "@MMK_R_a" = -130; "@MMK_R_h" = -70; "@MMK_R_n" = -80; "@MMK_R_o" = -140; "@MMK_R_s" = -120; "@MMK_R_v" = -100; "@MMK_R_w" = -100; "@MMK_R_y" = -90; }; "@MMK_L_U" = { "@MMK_R_A" = -20; }; "@MMK_L_V" = { "@MMK_R_A" = -10; "@MMK_R_M" = -20; "@MMK_R_O" = -10; "@MMK_R_S" = -20; "@MMK_R_a" = -60; "@MMK_R_o" = -60; }; "@MMK_L_W" = { "@MMK_R_o" = -50; }; "@MMK_L_X" = { "@MMK_R_O" = -30; "@MMK_R_o" = -30; }; "@MMK_L_Y" = { "@MMK_R_A" = -40; "@MMK_R_M" = -30; "@MMK_R_O" = -20; "@MMK_R_a" = -90; "@MMK_R_o" = -80; }; "@MMK_L_Z" = { "@MMK_R_O" = -30; "@MMK_R_a" = -50; "@MMK_R_o" = -50; "@MMK_R_u" = -30; }; "@MMK_L_a" = { "@MMK_R_T" = -120; "@MMK_R_V" = -50; "@MMK_R_Y" = -70; "@MMK_R_Z" = -20; }; "@MMK_L_c" = { "@MMK_R_o" = -10; }; "@MMK_L_comma" = { "@MMK_R_four" = -70; "@MMK_R_seven" = -100; "@MMK_R_six" = -30; "@MMK_R_space" = -50; "@MMK_R_y" = 30; "@MMK_R_zero" = -70; }; "@MMK_L_e" = { "@MMK_R_T" = -140; "@MMK_R_V" = -60; "@MMK_R_W" = -50; "@MMK_R_X" = -10; "@MMK_R_Y" = -80; "@MMK_R_Z" = -20; "@MMK_R_a" = -15; "@MMK_R_f" = -10; "@MMK_R_quoteright" = -20; "@MMK_R_t" = -10; "@MMK_R_v" = -10; "@MMK_R_y" = -10; "@MMK_R_z" = 0; }; "@MMK_L_eight" = { "@MMK_R_quote" = -10; }; "@MMK_L_f" = { "@MMK_R_a" = -30; "@MMK_R_g" = -40; "@MMK_R_o" = -30; "@MMK_R_quote" = 110; "@MMK_R_quoteright" = 80; }; "@MMK_L_five" = { "@MMK_R_quote" = -20; "@MMK_R_seven" = -30; }; "@MMK_L_four" = { "@MMK_R_comma" = -60; "@MMK_R_five" = -20; "@MMK_R_nine" = -20; "@MMK_R_one" = -20; "@MMK_R_quote" = -60; "@MMK_R_seven" = -50; "@MMK_R_two" = -20; }; "@MMK_L_g" = { "@MMK_R_quote" = 30; "@MMK_R_quoteright" = 40; }; "@MMK_L_i" = { "@MMK_R_quoteright" = 30; }; "@MMK_L_j" = { "@MMK_R_quoteright" = 30; }; "@MMK_L_k" = { "@MMK_R_a" = -10; "@MMK_R_quoteright" = 10; }; "@MMK_L_n" = { "@MMK_R_T" = -90; "@MMK_R_quote" = -30; "@MMK_R_v" = -10; }; "@MMK_L_nine" = { "@MMK_R_comma" = -70; "@MMK_R_two" = -30; }; "@MMK_L_o" = { "@MMK_R_A" = -10; "@MMK_R_J" = -20; "@MMK_R_T" = -140; "@MMK_R_V" = -60; "@MMK_R_W" = -50; "@MMK_R_X" = -30; "@MMK_R_Y" = -80; "@MMK_R_Z" = -40; "@MMK_R_f" = -10; "@MMK_R_j" = -15; "@MMK_R_quote" = -20; "@MMK_R_quoteright" = -20; "@MMK_R_t" = -10; "@MMK_R_v" = -20; "@MMK_R_x" = -20; "@MMK_R_y" = -10; "@MMK_R_z" = -10; }; "@MMK_L_one" = { "@MMK_R_quote" = -40; "@MMK_R_seven" = -30; "@MMK_R_six" = -20; "@MMK_R_zero" = -20; }; "@MMK_L_quote" = { "@MMK_R_A" = -60; "@MMK_R_eight" = -20; "@MMK_R_f" = 60; "@MMK_R_four" = -80; "@MMK_R_g" = -60; "@MMK_R_o" = -20; "@MMK_R_s" = -40; "@MMK_R_seven" = 20; "@MMK_R_six" = -20; "@MMK_R_t" = 60; "@MMK_R_zero" = -20; }; "@MMK_L_quoteright" = { "@MMK_R_A" = -60; "@MMK_R_O" = -30; "@MMK_R_f" = 20; "@MMK_R_g" = -60; "@MMK_R_i" = 30; "@MMK_R_j" = 30; "@MMK_R_o" = -40; "@MMK_R_s" = -70; "@MMK_R_t" = 40; }; "@MMK_L_r" = { "@MMK_R_a" = -20; "@MMK_R_g" = -30; "@MMK_R_o" = -20; "@MMK_R_quote" = 50; "@MMK_R_quoteright" = 30; "@MMK_R_s" = -10; }; "@MMK_L_s" = { "@MMK_R_T" = -120; "@MMK_R_quoteright" = 0; "@MMK_R_v" = -10; }; "@MMK_L_seven" = { "@MMK_R_comma" = -90; "@MMK_R_four" = -30; "@MMK_R_one" = 30; "@MMK_R_quote" = 20; "@MMK_R_six" = -20; "@MMK_R_zero" = -20; }; "@MMK_L_six" = { "@MMK_R_nine" = -20; "@MMK_R_quote" = -20; "@MMK_R_seven" = -20; }; "@MMK_L_t" = { "@MMK_R_a" = -20; "@MMK_R_o" = -20; "@MMK_R_quote" = 20; "@MMK_R_quoteright" = 30; }; "@MMK_L_three" = { "@MMK_R_seven" = -20; }; "@MMK_L_two" = { "@MMK_R_four" = -20; "@MMK_R_seven" = -10; }; "@MMK_L_v" = { "@MMK_R_A" = -20; "@MMK_R_T" = -100; "@MMK_R_a" = -20; "@MMK_R_g" = -30; "@MMK_R_o" = -20; "@MMK_R_s" = -25; }; "@MMK_L_w" = { "@MMK_R_A" = -20; "@MMK_R_T" = -100; }; "@MMK_L_x" = { "@MMK_R_o" = -20; }; "@MMK_L_y" = { "@MMK_R_A" = -10; "@MMK_R_T" = -90; "@MMK_R_a" = -15; "@MMK_R_comma" = -50; "@MMK_R_g" = -20; "@MMK_R_o" = -10; }; "@MMK_L_z" = { "@MMK_R_o" = -10; }; "@MMK_L_zero" = { "@MMK_R_comma" = -60; "@MMK_R_quote" = -20; }; }; "3E7589AA-8194-470F-8E2F-13C1C581BE24" = { "@MMK_L_A" = { "@MMK_R_J" = -20; "@MMK_R_O" = -30; "@MMK_R_T" = -80; "@MMK_R_U" = -20; "@MMK_R_V" = -50; "@MMK_R_Y" = -70; "@MMK_R_o" = -20; "@MMK_R_quote" = -60; "@MMK_R_quoteright" = -60; "@MMK_R_t" = -30; "@MMK_R_u" = -20; "@MMK_R_v" = -40; "@MMK_R_w" = -30; "@MMK_R_y" = -30; }; "@MMK_L_B" = { "@MMK_R_T" = -30; "@MMK_R_V" = -20; "@MMK_R_Y" = -50; "@MMK_R_v" = -10; }; "@MMK_L_C" = { "@MMK_R_O" = -30; "@MMK_R_o" = -10; "@MMK_R_quoteright" = 20; }; "@MMK_L_E" = { "@MMK_R_O" = -10; "@MMK_R_V" = -20; "@MMK_R_Y" = -20; }; "@MMK_L_F" = { "@MMK_R_A" = -40; "@MMK_R_O" = -20; "@MMK_R_a" = -50; "@MMK_R_o" = -40; }; "@MMK_L_K" = { "@MMK_R_O" = -40; "@MMK_R_a" = -40; "@MMK_R_o" = -45; "@MMK_R_v" = -40; "@MMK_R_y" = -30; }; "@MMK_L_L" = { "@MMK_R_O" = -40; "@MMK_R_T" = -130; "@MMK_R_U" = -30; "@MMK_R_V" = -80; "@MMK_R_Y" = -120; "@MMK_R_a" = -20; "@MMK_R_o" = -20; "@MMK_R_quote" = -100; "@MMK_R_quoteright" = -120; "@MMK_R_t" = -30; "@MMK_R_u" = -20; "@MMK_R_v" = -60; }; "@MMK_L_M" = { "@MMK_R_O" = -20; "@MMK_R_T" = -30; "@MMK_R_V" = -20; "@MMK_R_Y" = -30; "@MMK_R_o" = -20; "@MMK_R_t" = -20; "@MMK_R_v" = -20; }; "@MMK_L_O" = { "@MMK_R_A" = -40; "@MMK_R_J" = -20; "@MMK_R_M" = -20; "@MMK_R_T" = -50; "@MMK_R_V" = -20; "@MMK_R_X" = -40; "@MMK_R_Y" = -40; "@MMK_R_Z" = -30; "@MMK_R_quoteright" = -20; }; "@MMK_L_P" = { "@MMK_R_A" = -50; "@MMK_R_V" = -10; "@MMK_R_Y" = -20; "@MMK_R_a" = -20; "@MMK_R_o" = -30; }; "@MMK_L_R" = { "@MMK_R_O" = -20; "@MMK_R_T" = -30; "@MMK_R_U" = -10; "@MMK_R_V" = -30; "@MMK_R_Y" = -40; "@MMK_R_a" = -30; "@MMK_R_o" = -30; "@MMK_R_u" = -10; "@MMK_R_v" = -20; "@MMK_R_y" = -20; }; "@MMK_L_S" = { "@MMK_R_V" = -20; }; "@MMK_L_T" = { "@MMK_R_A" = -80; "@MMK_R_M" = -30; "@MMK_R_O" = -50; "@MMK_R_S" = -30; "@MMK_R_a" = -120; "@MMK_R_h" = -40; "@MMK_R_n" = -90; "@MMK_R_o" = -130; "@MMK_R_quote" = 40; "@MMK_R_quoteright" = 40; "@MMK_R_s" = -130; "@MMK_R_v" = -70; "@MMK_R_w" = -70; "@MMK_R_y" = -70; }; "@MMK_L_U" = { "@MMK_R_A" = -20; }; "@MMK_L_V" = { "@MMK_R_A" = -50; "@MMK_R_M" = -20; "@MMK_R_O" = -20; "@MMK_R_S" = -30; "@MMK_R_a" = -80; "@MMK_R_o" = -70; }; "@MMK_L_W" = { "@MMK_R_a" = -30; "@MMK_R_o" = -50; }; "@MMK_L_X" = { "@MMK_R_O" = -40; "@MMK_R_a" = -40; "@MMK_R_o" = -50; }; "@MMK_L_Y" = { "@MMK_R_A" = -70; "@MMK_R_M" = -30; "@MMK_R_O" = -40; "@MMK_R_a" = -100; "@MMK_R_o" = -110; }; "@MMK_L_Z" = { "@MMK_R_O" = -30; "@MMK_R_a" = -50; "@MMK_R_o" = -50; "@MMK_R_u" = -30; }; "@MMK_L_a" = { "@MMK_R_T" = -120; "@MMK_R_V" = -70; "@MMK_R_W" = -30; "@MMK_R_X" = -20; "@MMK_R_Y" = -110; "@MMK_R_Z" = -20; "@MMK_R_quote" = -30; }; "@MMK_L_c" = { "@MMK_R_o" = -20; }; "@MMK_L_comma" = { "@MMK_R_four" = -20; "@MMK_R_seven" = -100; "@MMK_R_space" = -40; "@MMK_R_y" = 30; "@MMK_R_zero" = -50; }; "@MMK_L_e" = { "@MMK_R_T" = -130; "@MMK_R_V" = -70; "@MMK_R_W" = -40; "@MMK_R_X" = -50; "@MMK_R_Y" = -110; "@MMK_R_Z" = -30; "@MMK_R_a" = -10; "@MMK_R_f" = -10; "@MMK_R_quote" = -30; "@MMK_R_quoteright" = -30; "@MMK_R_v" = -10; "@MMK_R_x" = -20; "@MMK_R_z" = -6; }; "@MMK_L_eight" = { "@MMK_R_quote" = -20; "@MMK_R_seven" = -10; }; "@MMK_L_f" = { "@MMK_R_a" = -20; "@MMK_R_g" = -30; "@MMK_R_o" = -25; "@MMK_R_quote" = 70; "@MMK_R_quoteright" = 70; }; "@MMK_L_five" = { "@MMK_R_quote" = 10; }; "@MMK_L_four" = { "@MMK_R_one" = -10; "@MMK_R_quote" = -50; "@MMK_R_seven" = -50; "@MMK_R_three" = -10; "@MMK_R_two" = -10; }; "@MMK_L_g" = { "@MMK_R_a" = -20; "@MMK_R_quoteright" = 30; "@MMK_R_y" = 20; }; "@MMK_L_i" = { "@MMK_R_quoteright" = 30; }; "@MMK_L_j" = { "@MMK_R_quoteright" = 30; }; "@MMK_L_k" = { "@MMK_R_a" = -20; "@MMK_R_o" = -10; "@MMK_R_quote" = 20; "@MMK_R_quoteright" = 20; "@MMK_R_v" = -10; }; "@MMK_L_l" = { "@MMK_R_quoteright" = -40; }; "@MMK_L_n" = { "@MMK_R_T" = -80; "@MMK_R_quote" = -20; "@MMK_R_v" = -15; }; "@MMK_L_nine" = { "@MMK_R_comma" = -50; "@MMK_R_two" = -20; }; "@MMK_L_o" = { "@MMK_R_A" = -20; "@MMK_R_J" = -20; "@MMK_R_M" = -20; "@MMK_R_T" = -130; "@MMK_R_V" = -70; "@MMK_R_W" = -40; "@MMK_R_X" = -50; "@MMK_R_Y" = -110; "@MMK_R_Z" = -20; "@MMK_R_f" = -10; "@MMK_R_j" = -15; "@MMK_R_quote" = -40; "@MMK_R_quoteright" = -20; "@MMK_R_v" = -20; "@MMK_R_x" = -30; "@MMK_R_y" = -25; "@MMK_R_z" = -10; }; "@MMK_L_one" = { "@MMK_R_four" = -20; "@MMK_R_quote" = -60; "@MMK_R_seven" = -20; "@MMK_R_six" = -20; "@MMK_R_zero" = -20; }; "@MMK_L_quote" = { "@MMK_R_A" = -60; "@MMK_R_J" = -20; "@MMK_R_T" = 40; "@MMK_R_a" = -20; "@MMK_R_eight" = -20; "@MMK_R_f" = 30; "@MMK_R_five" = 20; "@MMK_R_four" = -80; "@MMK_R_g" = -60; "@MMK_R_h" = 20; "@MMK_R_o" = -40; "@MMK_R_s" = -40; "@MMK_R_seven" = 20; "@MMK_R_t" = 40; "@MMK_R_v" = 20; "@MMK_R_w" = 20; "@MMK_R_y" = 20; "@MMK_R_zero" = -20; }; "@MMK_L_quoteright" = { "@MMK_R_A" = -60; "@MMK_R_O" = -20; "@MMK_R_T" = 40; "@MMK_R_a" = -10; "@MMK_R_f" = 20; "@MMK_R_g" = -50; "@MMK_R_i" = 30; "@MMK_R_j" = 30; "@MMK_R_o" = -40; "@MMK_R_s" = -40; "@MMK_R_t" = 50; }; "@MMK_L_r" = { "@MMK_R_a" = -30; "@MMK_R_g" = -25; "@MMK_R_o" = -20; "@MMK_R_quote" = 20; "@MMK_R_quoteright" = 20; "@MMK_R_s" = -10; }; "@MMK_L_s" = { "@MMK_R_T" = -110; "@MMK_R_quoteright" = 0; "@MMK_R_v" = -10; }; "@MMK_L_seven" = { "@MMK_R_comma" = -80; "@MMK_R_four" = -40; "@MMK_R_one" = 20; "@MMK_R_quote" = 20; "@MMK_R_three" = 10; "@MMK_R_zero" = -20; }; "@MMK_L_six" = { "@MMK_R_quote" = -20; }; "@MMK_L_t" = { "@MMK_R_a" = -10; "@MMK_R_quote" = 40; "@MMK_R_quoteright" = 30; }; "@MMK_L_three" = { "@MMK_R_seven" = -20; }; "@MMK_L_two" = { "@MMK_R_seven" = -20; }; "@MMK_L_v" = { "@MMK_R_A" = -30; "@MMK_R_M" = -20; "@MMK_R_T" = -70; "@MMK_R_a" = -20; "@MMK_R_g" = -40; "@MMK_R_o" = -20; "@MMK_R_quote" = 20; "@MMK_R_s" = -20; }; "@MMK_L_w" = { "@MMK_R_A" = -30; "@MMK_R_T" = -70; "@MMK_R_quote" = 20; }; "@MMK_L_x" = { "@MMK_R_o" = -30; }; "@MMK_L_y" = { "@MMK_R_A" = -40; "@MMK_R_T" = -60; "@MMK_R_a" = -20; "@MMK_R_comma" = -60; "@MMK_R_g" = -30; "@MMK_R_o" = -10; "@MMK_R_quote" = 20; }; "@MMK_L_z" = { "@MMK_R_o" = -10; }; "@MMK_L_zero" = { "@MMK_R_comma" = -30; "@MMK_R_quote" = -20; }; }; "BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = { "@MMK_L_A" = { "@MMK_R_J" = -10; "@MMK_R_O" = -40; "@MMK_R_T" = -80; "@MMK_R_U" = -40; "@MMK_R_V" = -70; "@MMK_R_Y" = -100; "@MMK_R_o" = -30; "@MMK_R_quote" = -60; "@MMK_R_quoteright" = -50; "@MMK_R_t" = -60; "@MMK_R_u" = -30; "@MMK_R_v" = -50; "@MMK_R_w" = -40; "@MMK_R_y" = -40; }; "@MMK_L_B" = { "@MMK_R_T" = -20; "@MMK_R_V" = -30; "@MMK_R_Y" = -50; "@MMK_R_v" = -10; "@MMK_R_y" = -10; }; "@MMK_L_C" = { "@MMK_R_O" = -30; "@MMK_R_o" = -10; "@MMK_R_quoteright" = 10; }; "@MMK_L_E" = { "@MMK_R_O" = -10; "@MMK_R_V" = -10; "@MMK_R_Y" = -20; }; "@MMK_L_F" = { "@MMK_R_A" = -50; "@MMK_R_O" = -10; "@MMK_R_a" = -20; "@MMK_R_o" = -15; }; "@MMK_L_K" = { "@MMK_R_O" = -40; "@MMK_R_a" = -20; "@MMK_R_o" = -50; "@MMK_R_v" = -60; "@MMK_R_y" = -50; }; "@MMK_L_L" = { "@MMK_R_O" = -20; "@MMK_R_T" = -110; "@MMK_R_U" = -20; "@MMK_R_V" = -80; "@MMK_R_Y" = -140; "@MMK_R_quote" = -70; "@MMK_R_quoteright" = -70; "@MMK_R_t" = -20; "@MMK_R_v" = -40; }; "@MMK_L_M" = { "@MMK_R_O" = -30; "@MMK_R_T" = -50; "@MMK_R_V" = -50; "@MMK_R_Y" = -70; "@MMK_R_o" = -30; "@MMK_R_t" = -40; "@MMK_R_v" = -40; "@MMK_R_y" = -40; }; "@MMK_L_O" = { "@MMK_R_A" = -40; "@MMK_R_J" = -20; "@MMK_R_M" = -30; "@MMK_R_T" = -40; "@MMK_R_V" = -50; "@MMK_R_X" = -50; "@MMK_R_Y" = -70; "@MMK_R_Z" = -20; }; "@MMK_L_P" = { "@MMK_R_A" = -50; "@MMK_R_V" = -20; "@MMK_R_Y" = -50; "@MMK_R_a" = -20; "@MMK_R_o" = -20; }; "@MMK_L_R" = { "@MMK_R_O" = -20; "@MMK_R_T" = -20; "@MMK_R_U" = -10; "@MMK_R_V" = -30; "@MMK_R_Y" = -50; "@MMK_R_a" = -20; "@MMK_R_o" = -20; "@MMK_R_v" = -20; "@MMK_R_y" = -20; }; "@MMK_L_T" = { "@MMK_R_A" = -80; "@MMK_R_M" = -50; "@MMK_R_O" = -40; "@MMK_R_S" = -20; "@MMK_R_a" = -60; "@MMK_R_h" = -10; "@MMK_R_n" = -20; "@MMK_R_o" = -100; "@MMK_R_quote" = 30; "@MMK_R_s" = -60; "@MMK_R_v" = -20; "@MMK_R_w" = -20; }; "@MMK_L_U" = { "@MMK_R_A" = -40; }; "@MMK_L_V" = { "@MMK_R_A" = -70; "@MMK_R_M" = -50; "@MMK_R_O" = -40; "@MMK_R_S" = -20; "@MMK_R_a" = -60; "@MMK_R_o" = -70; "@MMK_R_quote" = 20; }; "@MMK_L_W" = { "@MMK_R_o" = -40; "@MMK_R_quote" = 20; }; "@MMK_L_X" = { "@MMK_R_O" = -50; "@MMK_R_a" = -30; "@MMK_R_o" = -50; }; "@MMK_L_Y" = { "@MMK_R_A" = -100; "@MMK_R_M" = -70; "@MMK_R_O" = -70; "@MMK_R_a" = -100; "@MMK_R_o" = -120; }; "@MMK_L_Z" = { "@MMK_R_O" = -20; "@MMK_R_a" = -10; "@MMK_R_o" = -20; "@MMK_R_u" = -20; }; "@MMK_L_a" = { "@MMK_R_T" = -40; "@MMK_R_V" = -60; "@MMK_R_X" = -20; "@MMK_R_Y" = -120; "@MMK_R_t" = -10; "@MMK_R_v" = -10; "@MMK_R_y" = -10; }; "@MMK_L_c" = { "@MMK_R_o" = -15; }; "@MMK_L_comma" = { "@MMK_R_seven" = -110; "@MMK_R_space" = -30; "@MMK_R_zero" = -20; }; "@MMK_L_e" = { "@MMK_R_T" = -100; "@MMK_R_V" = -70; "@MMK_R_W" = -40; "@MMK_R_X" = -30; "@MMK_R_Y" = -120; "@MMK_R_a" = -15; "@MMK_R_f" = -10; "@MMK_R_quote" = 0; "@MMK_R_v" = -20; "@MMK_R_x" = -20; "@MMK_R_y" = -20; }; "@MMK_L_eight" = { "@MMK_R_quote" = -10; "@MMK_R_seven" = -20; }; "@MMK_L_f" = { "@MMK_R_a" = -10; "@MMK_R_g" = -20; "@MMK_R_o" = -10; "@MMK_R_quote" = 40; "@MMK_R_quoteright" = 70; }; "@MMK_L_five" = { "@MMK_R_nine" = -10; "@MMK_R_seven" = -10; }; "@MMK_L_four" = { "@MMK_R_quote" = -20; "@MMK_R_seven" = -30; }; "@MMK_L_g" = { "@MMK_R_a" = -10; "@MMK_R_o" = -10; "@MMK_R_quote" = 20; "@MMK_R_quoteright" = 30; "@MMK_R_y" = 10; }; "@MMK_L_i" = { "@MMK_R_quoteright" = 60; }; "@MMK_L_j" = { "@MMK_R_quoteright" = 40; }; "@MMK_L_k" = { "@MMK_R_a" = -15; "@MMK_R_o" = -25; "@MMK_R_quoteright" = 20; }; "@MMK_L_l" = { "@MMK_R_quoteright" = 20; }; "@MMK_L_n" = { "@MMK_R_T" = -30; "@MMK_R_quote" = -20; "@MMK_R_v" = -10; }; "@MMK_L_nine" = { "@MMK_R_comma" = -20; "@MMK_R_seven" = -10; }; "@MMK_L_o" = { "@MMK_R_A" = -30; "@MMK_R_J" = -20; "@MMK_R_M" = -30; "@MMK_R_T" = -100; "@MMK_R_V" = -70; "@MMK_R_W" = -40; "@MMK_R_X" = -50; "@MMK_R_Y" = -120; "@MMK_R_Z" = -10; "@MMK_R_f" = -10; "@MMK_R_j" = -20; "@MMK_R_quote" = -20; "@MMK_R_quoteright" = 0; "@MMK_R_t" = -10; "@MMK_R_v" = -30; "@MMK_R_x" = -35; "@MMK_R_y" = -20; "@MMK_R_z" = -10; }; "@MMK_L_one" = { "@MMK_R_quote" = -50; "@MMK_R_seven" = -40; "@MMK_R_zero" = -20; }; "@MMK_L_quote" = { "@MMK_R_A" = -70; "@MMK_R_T" = 30; "@MMK_R_V" = 20; "@MMK_R_W" = 20; "@MMK_R_eight" = -10; "@MMK_R_f" = 20; "@MMK_R_four" = -20; "@MMK_R_g" = -20; "@MMK_R_h" = 10; "@MMK_R_o" = -20; "@MMK_R_seven" = 20; "@MMK_R_six" = -20; "@MMK_R_t" = 20; "@MMK_R_zero" = -20; }; "@MMK_L_quoteright" = { "@MMK_R_A" = -70; "@MMK_R_O" = -30; "@MMK_R_a" = -20; "@MMK_R_g" = -40; "@MMK_R_h" = 20; "@MMK_R_i" = 30; "@MMK_R_j" = 20; "@MMK_R_o" = -50; "@MMK_R_s" = -30; "@MMK_R_t" = 10; }; "@MMK_L_r" = { "@MMK_R_a" = -10; "@MMK_R_g" = -10; "@MMK_R_o" = -5; "@MMK_R_quoteright" = 20; "@MMK_R_s" = -10; }; "@MMK_L_seven" = { "@MMK_R_comma" = -60; "@MMK_R_four" = -30; "@MMK_R_one" = 20; "@MMK_R_quote" = 20; "@MMK_R_six" = -20; }; "@MMK_L_six" = { "@MMK_R_nine" = -20; "@MMK_R_quote" = -20; "@MMK_R_seven" = -20; }; "@MMK_L_t" = { "@MMK_R_a" = -10; "@MMK_R_o" = -5; "@MMK_R_quote" = 20; "@MMK_R_quoteright" = 30; }; "@MMK_L_three" = { "@MMK_R_seven" = -30; }; "@MMK_L_two" = { "@MMK_R_seven" = -20; }; "@MMK_L_v" = { "@MMK_R_A" = -50; "@MMK_R_M" = -40; "@MMK_R_T" = -20; "@MMK_R_g" = -35; "@MMK_R_o" = -30; "@MMK_R_s" = -25; }; "@MMK_L_w" = { "@MMK_R_A" = -40; "@MMK_R_T" = -20; }; "@MMK_L_x" = { "@MMK_R_o" = -35; }; "@MMK_L_y" = { "@MMK_R_A" = -60; "@MMK_R_M" = -40; "@MMK_R_comma" = -40; "@MMK_R_g" = -30; "@MMK_R_o" = 0; }; "@MMK_L_z" = { "@MMK_R_o" = -10; }; "@MMK_L_zero" = { "@MMK_R_comma" = -20; "@MMK_R_quote" = -20; "@MMK_R_two" = -10; }; }; }; unitsPerEm = 1000; userData = { AsteriskParameters = { "253E7231-480D-4F8E-8754-50FC8575C08E" = ( "754", "30", 7, 51, "80", "50" ); }; GSDimensionPlugin.Dimensions = { "3E7589AA-8194-470F-8E2F-13C1C581BE24" = { HH = 91; HV = 93; OH = 91; OV = 93; arAlef = 86; arBar = 92; nV = 90; oH = 88; }; "BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = { HH = 215; HV = 225; nV = 220; oH = 210; }; "C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = { HH = 18; HV = 19; nV = 17; oH = 16; }; }; uniTestValue = def; }; versionMajor = 1; versionMinor = 0; } glyphslib-2.2.1/tests/glyphdata_test.py000066400000000000000000000067461322341616200202560ustar00rootroot00000000000000# -*- coding=utf-8 -*- # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from glyphsLib.glyphdata import get_glyph import unittest class GlyphDataTest(unittest.TestCase): def test_production_name(self): prod = lambda n: get_glyph(n).production_name self.assertEqual(prod(".notdef"), ".notdef") self.assertEqual(prod("eacute"), "eacute") self.assertEqual(prod("Abreveacute"), "uni1EAE") self.assertEqual(prod("C-fraktur"), "uni212D") self.assertEqual(prod("Dboldscript-math"), "u1D4D3") self.assertEqual(prod("fi"), "fi") self.assertEqual(prod("s_t"), "s_t") self.assertEqual(prod("Gcommaaccent"), "uni0122") self.assertEqual(prod("o_f_f_i.foo"), "o_f_f_i.foo") def test_unicode(self): uni = lambda n: get_glyph(n).unicode self.assertIsNone(uni(".notdef")) self.assertEqual(uni("eacute"), "é") self.assertEqual(uni("Abreveacute"), "Ắ") self.assertEqual(uni("C-fraktur"), "ℭ") self.assertEqual(uni("Dboldscript-math"), "𝓓") self.assertEqual(uni("fi"), "fi") self.assertIsNone(uni("s_t")) # no 'unicode' in GlyphsData self.assertEqual(uni("Gcommaaccent"), "Ģ") self.assertEqual(uni("o_f_f_i.foo"), "offi") def test_category(self): cat = lambda n: (get_glyph(n).category, get_glyph(n).subCategory) self.assertEqual(cat(".notdef"), ("Separator", None)) self.assertEqual(cat("uni000D"), ("Separator", None)) self.assertEqual(cat("boxHeavyUp"), ("Symbol", "Geometry")) self.assertEqual(cat("eacute"), ("Letter", "Lowercase")) self.assertEqual(cat("Abreveacute"), ("Letter", "Uppercase")) self.assertEqual(cat("C-fraktur"), ("Letter", "Uppercase")) self.assertEqual(cat("fi"), ("Letter", "Ligature")) self.assertEqual(cat("fi.alt"), ("Letter", "Ligature")) self.assertEqual(cat("hib-ko"), ("Letter", "Syllable")) self.assertEqual(cat("one.foo"), ("Number", "Decimal Digit")) self.assertEqual(cat("one_two.foo"), ("Number", "Ligature")) self.assertEqual(cat("o_f_f_i"), ("Letter", "Ligature")) self.assertEqual(cat("o_f_f_i.foo"), ("Letter", "Ligature")) self.assertEqual(cat("ain_alefMaksura-ar.fina"), ("Letter", "Ligature")) def test_bug232(self): # https://github.com/googlei18n/glyphsLib/issues/232 u, g = get_glyph("uni07F0"), get_glyph("longlowtonecomb-nko") self.assertEqual((u.category, g.category), ("Mark", "Mark")) self.assertEqual((u.subCategory, g.subCategory), ("Nonspacing", "Nonspacing")) self.assertEqual((u.production_name, g.production_name), ("uni07F0", "uni07F0")) self.assertEqual((u.unicode, g.unicode), ("\u07F0", "\u07F0")) if __name__ == "__main__": unittest.main() glyphslib-2.2.1/tests/interpolation_test.py000066400000000000000000000424601322341616200211610ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2017 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) import difflib import os.path import shutil import sys import tempfile import unittest import xml.etree.ElementTree as etree import defcon from fontTools.misc.py23 import open from glyphsLib.builder.constants import GLYPHS_PREFIX from glyphsLib.interpolation import ( build_designspace, set_weight_class, set_width_class, build_stylemap_names ) from glyphsLib.classes import GSInstance, GSCustomParameter def makeFamily(familyName): m1 = makeMaster(familyName, "Regular", weight=90.0) m2 = makeMaster(familyName, "Black", weight=190.0) instances = { "data": [ makeInstance("Regular", weight=("Regular", 400, 90)), makeInstance("Semibold", weight=("Semibold", 600, 128)), makeInstance("Bold", weight=("Bold", 700, 151), is_bold=True), makeInstance("Black", weight=("Black", 900, 190)), ], } return [m1, m2], instances def makeMaster(familyName, styleName, weight=None, width=None): m = defcon.Font() m.info.familyName, m.info.styleName = familyName, styleName if weight is not None: m.lib[GLYPHS_PREFIX + "weightValue"] = weight if width is not None: m.lib[GLYPHS_PREFIX + "widthValue"] = width return m def makeInstance(name, weight=None, width=None, is_bold=None, is_italic=None, linked_style=None): inst = GSInstance() inst.name = name if weight is not None: # Glyphs 2.3 stores the instance weight in two to three places: # 1. as a textual weightClass (such as “Bold”; no value defaults to # "Regular"); # 2. (optional) as numeric customParameters.weightClass (such as 700), # which corresponds to OS/2.usWeightClass where 100 means Thin, # 400 means Regular, 700 means Bold, and 900 means Black; # 3. as numeric interpolationWeight (such as 66.0), which typically is # the stem width but can be anything that works for interpolation # (no value defaults to 100). weightName, weightClass, interpolationWeight = weight if weightName is not None: inst.weightClass = weightName if weightClass is not None: inst.customParameters["weightClass"] = weightClass if interpolationWeight is not None: inst.interpolationWeight = interpolationWeight if width is not None: # Glyphs 2.3 stores the instance width in two places: # 1. as a textual widthClass (such as “Condensed”; no value defaults # to "Medium (normal)"); # 2. as numeric interpolationWidth (such as 79), which typically is # a percentage of whatever the font designer considers “normal” # but can be anything that works for interpolation (no value # defaults to 100). widthClass, interpolationWidth = width if widthClass is not None: inst.widthClass = widthClass if interpolationWidth is not None: inst.interpolationWidth = interpolationWidth # TODO: Support custom axes; need to triple-check how these are encoded in # Glyphs files. Glyphs 3 will likely overhaul the representation of axes. if is_bold is not None: inst.isBold = is_bold if is_italic is not None: inst.isItalic = is_italic if linked_style is not None: inst.linkStyle = linked_style return inst class DesignspaceTest(unittest.TestCase): def build_designspace(self, masters, instances): master_dir = tempfile.mkdtemp() try: designspace, _ = build_designspace( masters, master_dir, os.path.join(master_dir, "out"), instances) with open(designspace, mode="r", encoding="utf-8") as f: result = f.readlines() finally: shutil.rmtree(master_dir) return result def expect_designspace(self, masters, instances, expectedFile): actual = self.build_designspace(masters, instances) path, _ = os.path.split(__file__) expectedPath = os.path.join(path, "data", expectedFile) with open(expectedPath, mode="r", encoding="utf-8") as f: expected = f.readlines() if os.path.sep == '\\': # On windows, the test must not fail because of a difference between # forward and backward slashes in filname paths. # The failure happens because of line 217 of "mutatorMath\ufo\document.py" # > pathRelativeToDocument = os.path.relpath(fileName, os.path.dirname(self.path)) expected = [line.replace('filename="out/', 'filename="out\\') for line in expected] if actual != expected: for line in difflib.unified_diff( expected, actual, fromfile=expectedPath, tofile=""): sys.stderr.write(line) self.fail("*.designspace file is different from expected") def test_basic(self): masters, instances = makeFamily("DesignspaceTest Basic") self.expect_designspace(masters, instances, "DesignspaceTestBasic.designspace") def test_inactive_from_exports(self): # Glyphs.app recognizes exports=0 as a flag for inactive instances. # https://github.com/googlei18n/glyphsLib/issues/129 masters, instances = makeFamily("DesignspaceTest Inactive") for inst in instances["data"]: if inst.name != "Semibold": inst.exports = False self.expect_designspace(masters, instances, "DesignspaceTestInactive.designspace") def test_familyName(self): masters, instances = makeFamily("DesignspaceTest FamilyName") customFamily = makeInstance("Regular", weight=("Bold", 600, 151)) customFamily.customParameters["familyName"] = "Custom Family" instances["data"] = [ makeInstance("Regular", weight=("Regular", 400, 90)), customFamily, ] self.expect_designspace(masters, instances, "DesignspaceTestFamilyName.designspace") def test_fileName(self): masters, instances = makeFamily("DesignspaceTest FamilyName") customFileName= makeInstance("Regular", weight=("Bold", 600, 151)) customFileName.customParameters["fileName"] = "Custom FileName" instances["data"] = [ makeInstance("Regular", weight=("Regular", 400, 90)), customFileName, ] self.expect_designspace(masters, instances, "DesignspaceTestFileName.designspace") def test_noRegularMaster(self): # Currently, fonttools.varLib fails to build variable fonts # if the default axis value does not happen to be at the # location of one of the interpolation masters. # glyhpsLib tries to work around this downstream limitation. masters = [ makeMaster("NoRegularMaster", "Thin", weight=26), makeMaster("NoRegularMaster", "Black", weight=190), ] instances = {"data": [ makeInstance("Black", weight=("Black", 900, 190)), makeInstance("Regular", weight=("Regular", 400, 90)), makeInstance("Bold", weight=("Thin", 100, 26)), ]} doc = etree.fromstringlist(self.build_designspace(masters, instances)) weightAxis = doc.find('axes/axis[@tag="wght"]') self.assertEqual(weightAxis.attrib["minimum"], "100.0") self.assertEqual(weightAxis.attrib["default"], "100.0") # not 400 self.assertEqual(weightAxis.attrib["maximum"], "900.0") def test_postscriptFontName(self): master = makeMaster("PSNameTest", "Master") thin, black = makeInstance("Thin"), makeInstance("Black") instances = {"data": [thin, black]} black.customParameters["postscriptFontName"] = "PSNameTest-Superfat" d = etree.fromstringlist(self.build_designspace([master], instances)) def psname(doc, style): inst = doc.find('instances/instance[@stylename="%s"]' % style) return inst.attrib.get('postscriptfontname') self.assertIsNone(psname(d, "Thin")) self.assertEqual(psname(d, "Black"), "PSNameTest-Superfat") def test_instanceOrder(self): # The generated *.designspace file should place instances # in the same order as they appear in the original source. # https://github.com/googlei18n/glyphsLib/issues/113 masters, instances = makeFamily("DesignspaceTest InstanceOrder") instances["data"] = [ makeInstance("Black", weight=("Black", 900, 190)), makeInstance("Regular", weight=("Regular", 400, 90)), makeInstance("Bold", weight=("Bold", 700, 151), is_bold=True), ] self.expect_designspace(masters, instances, "DesignspaceTestInstanceOrder.designspace") def test_twoAxes(self): # In NotoSansArabic-MM.glyphs, the regular width only contains # parameters for the weight axis. For the width axis, glyphsLib # should use 100 as default value (just like Glyphs.app does). familyName = "DesignspaceTest TwoAxes" masters = [ makeMaster(familyName, "Regular", weight=90), makeMaster(familyName, "Black", weight=190), makeMaster(familyName, "Thin", weight=26), makeMaster(familyName, "ExtraCond", weight=90, width=70), makeMaster(familyName, "ExtraCond Black", weight=190, width=70), makeMaster(familyName, "ExtraCond Thin", weight=26, width=70), ] instances = { "data": [ makeInstance("Thin", weight=("Thin", 100, 26)), makeInstance("Regular", weight=("Regular", 400, 90)), makeInstance("Semibold", weight=("Semibold", 600, 128)), makeInstance("Black", weight=("Black", 900, 190)), makeInstance("ExtraCondensed Thin", weight=("Thin", 100, 26), width=("Extra Condensed", 70)), makeInstance("ExtraCondensed", weight=("Regular", 400, 90), width=("Extra Condensed", 70)), makeInstance("ExtraCondensed Black", weight=("Black", 900, 190), width=("Extra Condensed", 70)), ] } self.expect_designspace(masters, instances, "DesignspaceTestTwoAxes.designspace") def test_variationFontOrigin(self): # Glyphs 2.4.1 introduced a custom parameter “Variation Font Origin” # to specify which master should be considered the origin. # https://glyphsapp.com/blog/glyphs-2-4-1-released masters = [ makeMaster("Family", "Thin", weight=26), makeMaster("Family", "Regular", weight=100), makeMaster("Family", "Medium", weight=111), makeMaster("Family", "Black", weight=190), ] instances = { "data": [ makeInstance("Black", weight=("Black", 900, 190)), makeInstance("Medium", weight=("Medium", 444, 111)), makeInstance("Regular", weight=("Regular", 400, 100)), makeInstance("Thin", weight=("Thin", 100, 26)), ], "Variation Font Origin": "Medium", } doc = etree.fromstringlist(self.build_designspace(masters, instances)) medium = doc.find('sources/source[@stylename="Medium"]') self.assertEqual(medium.find("lib").attrib["copy"], "1") weightAxis = doc.find('axes/axis[@tag="wght"]') self.assertEqual(weightAxis.attrib["default"], "444.0") def test_designspace_name(self): master_dir = tempfile.mkdtemp() try: designspace_path, _ = build_designspace( [ makeMaster("Family Name", "Regular", weight=100), makeMaster("Family Name", "Bold", weight=190), ], master_dir, os.path.join(master_dir, "out"), {}) # no shared base style name, only write the family name self.assertEqual(os.path.basename(designspace_path), "FamilyName.designspace") designspace_path, _ = build_designspace( [ makeMaster("Family Name", "Italic", weight=100), makeMaster("Family Name", "Bold Italic", weight=190), ], master_dir, os.path.join(master_dir, "out"), {}) # 'Italic' is the base style; append to designspace name self.assertEqual(os.path.basename(designspace_path), "FamilyName-Italic.designspace") finally: shutil.rmtree(master_dir) WEIGHT_CLASS_KEY = GLYPHS_PREFIX + "weightClass" WIDTH_CLASS_KEY = GLYPHS_PREFIX + "widthClass" class SetWeightWidthClassesTest(unittest.TestCase): def test_no_weigth_class(self): ufo = defcon.Font() # name here says "Bold", however no excplit weightClass # is assigned set_weight_class(ufo, makeInstance("Bold")) # the default OS/2 weight class is set self.assertEqual(ufo.info.openTypeOS2WeightClass, 400) # non-empty value is stored in the UFO lib even if same as default self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Regular") def test_weight_class(self): ufo = defcon.Font() data = makeInstance( "Bold", weight=("Bold", None, 150) ) set_weight_class(ufo, data) self.assertEqual(ufo.info.openTypeOS2WeightClass, 700) self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Bold") def test_explicit_default_weight(self): ufo = defcon.Font() data = makeInstance( "Regular", weight=("Regular", None, 100) ) set_weight_class(ufo, data) # the default OS/2 weight class is set self.assertEqual(ufo.info.openTypeOS2WeightClass, 400) # non-empty value is stored in the UFO lib even if same as default self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Regular") def test_no_width_class(self): ufo = defcon.Font() # no explicit widthClass set, instance name doesn't matter set_width_class(ufo, makeInstance("Normal")) # the default OS/2 width class is set self.assertEqual(ufo.info.openTypeOS2WidthClass, 5) # non-empty value is stored in the UFO lib even if same as default self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Medium (normal)") def test_width_class(self): ufo = defcon.Font() data = makeInstance( "Condensed", width=("Condensed", 80) ) set_width_class(ufo, data) self.assertEqual(ufo.info.openTypeOS2WidthClass, 3) self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Condensed") def test_explicit_default_width(self): ufo = defcon.Font() data = makeInstance( "Regular", width=("Medium (normal)", 100) ) set_width_class(ufo, data) # the default OS/2 width class is set self.assertEqual(ufo.info.openTypeOS2WidthClass, 5) # non-empty value is stored in the UFO lib even if same as default self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Medium (normal)") def test_weight_and_width_class(self): ufo = defcon.Font() data = makeInstance( "SemiCondensed ExtraBold", weight=("ExtraBold", None, 160), width=("SemiCondensed", 90) ) set_weight_class(ufo, data) set_width_class(ufo, data) self.assertEqual(ufo.info.openTypeOS2WeightClass, 800) self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "ExtraBold") self.assertEqual(ufo.info.openTypeOS2WidthClass, 4) self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "SemiCondensed") def test_unknown_weight_class(self): ufo = defcon.Font() # "DemiLight" is not among the predefined weight classes listed in # Glyphs.app/Contents/Frameworks/GlyphsCore.framework/Versions/A/ # Resources/weights.plist # NOTE It is not possible from the user interface to set a custom # string as instance 'weightClass' since the choice is constrained # by a drop-down menu. data = makeInstance( "DemiLight Italic", weight=("DemiLight", 350, 70) ) set_weight_class(ufo, data) # we do not set any OS/2 weight class; user needs to provide # a 'weightClass' custom parameter in this special case self.assertTrue(ufo.info.openTypeOS2WeightClass is None) if __name__ == "__main__": sys.exit(unittest.main()) glyphslib-2.2.1/tests/main_test.py000066400000000000000000000030301322341616200172040ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) import unittest import subprocess import os import test_helpers class MainTest(unittest.TestCase, test_helpers.AssertLinesEqual): def test_parser_main(self): """This is both a test for the "main" functionality of glyphsLib.parser and for the round-trip of GlyphsUnitTestSans.glyphs. """ filename = os.path.join( os.path.dirname(__file__), 'data/GlyphsUnitTestSans.glyphs') with open(filename) as f: expected = f.read() out = subprocess.check_output( ['python', '-m', 'glyphsLib.parser', filename], universal_newlines=True) # Windows gives \r\n otherwise self.assertLinesEqual( str(expected.splitlines()), str(out.splitlines()), 'The roundtrip should output the .glyphs file unmodified.') glyphslib-2.2.1/tests/parser_test.py000066400000000000000000000126071322341616200175660ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import (print_function, division, absolute_import, unicode_literals) from collections import OrderedDict import unittest import datetime from glyphsLib.parser import Parser from glyphsLib.classes import GSGlyph, GSLayer from glyphsLib.types import color, glyphs_datetime from fontTools.misc.py23 import unicode GLYPH_DATA = '''\ ( { glyphname="A"; color=5; lastChange = "2017-04-30 13:57:04 +0000"; layers = (); leftKerningGroup = A; rightKerningGroup = A; unicode = 0041; } )''' class ParserTest(unittest.TestCase): def run_test(self, text, expected): parser = Parser() self.assertEqual(parser.parse(text), OrderedDict(expected)) def test_parse(self): self.run_test( '{myval=1; mylist=(1,2,3);}', [('myval', 1), ('mylist', [1, 2, 3])]) def test_trim_value(self): self.run_test( '{mystr="a\\"s\\077d\\U2019f";}', [('mystr', 'a"s?d’f')]) def test_trailing_content(self): with self.assertRaises(ValueError): self.run_test( '{myval=1;}trailing', [('myval', '1')]) def test_unexpected_content(self): with self.assertRaises(ValueError): self.run_test( '{myval=@unexpected;}', [('myval', '@unexpected')]) def test_with_utf8(self): self.run_test( b'{mystr="Don\xe2\x80\x99t crash";}', [('mystr', 'Don’t crash')]) def test_parse_str_infinity(self): self.run_test( b'{mystr = infinity;}', [('mystr', 'infinity')] ) self.run_test( b'{mystr = Infinity;}', [('mystr', 'Infinity')] ) self.run_test( b'{mystr = InFiNItY;}', [('mystr', 'InFiNItY')] ) def test_parse_str_inf(self): self.run_test( b'{mystr = inf;}', [('mystr', 'inf')] ) self.run_test( b'{mystr = Inf;}', [('mystr', 'Inf')] ) def test_parse_str_nan(self): self.run_test( b'{mystr = nan;}', [('mystr', 'nan')] ) self.run_test( b'{mystr = NaN;}', [('mystr', 'NaN')] ) def test_dont_crash_on_string_that_looks_like_a_dict(self): # https://github.com/googlei18n/glyphsLib/issues/238 self.run_test( b'{UUID0 = "{0.5, 0.5}";}', [('UUID0', '{0.5, 0.5}')] ) def test_parse_dict_in_dict(self): self.run_test( b'{outer = {inner = "turtles";};}', [('outer', OrderedDict([('inner', 'turtles')]))] ) GLYPH_ATTRIBUTES = { "bottomKerningGroup": str, "bottomMetricsKey": str, "category": str, "color": color, "export": bool, # "glyphname": str, "lastChange": glyphs_datetime, "layers": GSLayer, "leftKerningGroup": str, "leftMetricsKey": str, "name": str, "note": unicode, "partsSettings": dict, "production": str, "rightKerningGroup": str, "rightMetricsKey": str, "script": str, "subCategory": str, "topKerningGroup": str, "topMetricsKey": str, "unicode": str, "userData": dict, "vertWidthMetricsKey": str, "widthMetricsKey": str, } class ParserGlyphTest(unittest.TestCase): def test_parse_empty_glyphs(self): # data = '({glyphname="A";})' data = '({})' parser = Parser(GSGlyph) result = parser.parse(data) self.assertEqual(len(result), 1) glyph = result[0] self.assertIsInstance(glyph, GSGlyph) defaults_as_none = [ "category", "color", "lastChange", "leftKerningGroup", "leftMetricsKey", "name", "note", "rightKerningGroup", "rightMetricsKey", "script", "subCategory", "unicode", "widthMetricsKey", ] for attr in defaults_as_none: self.assertIsNone(getattr(glyph, attr)) self.assertIsNotNone(glyph.userData) defaults_as_true = [ "export", ] for attr in defaults_as_true: self.assertTrue(getattr(glyph, attr)) def test_parse_glyphs(self): data = GLYPH_DATA parser = Parser(GSGlyph) result = parser.parse(data) glyph = result[0] self.assertEqual(glyph.name, "A") self.assertEqual(glyph.color, 5) self.assertEqual(glyph.lastChange, datetime.datetime(2017, 4, 30, 13, 57, 4)) self.assertEqual(glyph.leftKerningGroup, "A") self.assertEqual(glyph.rightKerningGroup, "A") self.assertEqual(glyph.unicode, "0041") if __name__ == '__main__': unittest.main() glyphslib-2.2.1/tests/roundtrip_test.py000066400000000000000000000024371322341616200203200ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import unittest import os import glyphsLib from glyphsLib import classes import test_helpers class UFORoundtripTest(unittest.TestCase, test_helpers.AssertUFORoundtrip): def test_empty_font(self): empty_font = classes.GSFont() empty_font.masters.append(classes.GSFontMaster()) self.assertUFORoundtrip(empty_font) def test_GlyphsUnitTestSans(self): self.skipTest("TODO") filename = os.path.join(os.path.dirname(__file__), 'data/GlyphsUnitTestSans.glyphs') with open(filename) as f: font = glyphsLib.load(f) self.assertUFORoundtrip(font) if __name__ == '__main__': unittest.main() glyphslib-2.2.1/tests/run_roundtrip_on_noto.py000066400000000000000000000041421322341616200216730ustar00rootroot00000000000000# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import subprocess import os import unittest import re import test_helpers NOTO_DIRECTORY = os.path.join(os.path.dirname(__file__), 'noto-source-moyogo') NOTO_GIT_URL = "https://github.com/moyogo/noto-source.git" NOTO_GIT_BRANCH = "normalized-1071" APP_VERSION_RE = re.compile('\\.appVersion = "(.*)"') def glyphs_files(directory): for root, _dirs, files in os.walk(directory): for filename in files: if filename.endswith('.glyphs'): yield os.path.join(root, filename) def app_version(filename): with open(filename) as fp: for line in fp: m = APP_VERSION_RE.match(line) if m: return m.group(1) return "no_version" class NotoRoundtripTest(unittest.TestCase, test_helpers.AssertParseWriteRoundtrip): pass if not os.path.exists(NOTO_DIRECTORY): subprocess.call(["git", "clone", NOTO_GIT_URL, NOTO_DIRECTORY]) subprocess.check_call( ["git", "-C", NOTO_DIRECTORY, "checkout", NOTO_GIT_BRANCH]) for index, filename in enumerate(glyphs_files(NOTO_DIRECTORY)): def test_method(self, filename=filename): self.assertParseWriteRoundtrip(filename) file_basename = os.path.basename(filename) test_name = "test_n{0:0>3d}_v{1}_{2}".format( index, app_version(filename), file_basename.replace(r'[^a-zA-Z]', '')) test_method.__name__ = test_name setattr(NotoRoundtripTest, test_name, test_method) if __name__ == '__main__': import sys sys.exit(unittest.main()) glyphslib-2.2.1/tests/test_helpers.py000066400000000000000000000054771322341616200177430ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import difflib import sys from textwrap import dedent import glyphsLib from glyphsLib.builder import to_glyphs, to_ufos from glyphsLib.writer import Writer from fontTools.misc.py23 import UnicodeIO def write_to_lines(glyphs_object): """ Use the Writer to write the given object to a UnicodeIO. Return an array of lines ready for diffing. """ string = UnicodeIO() writer = Writer(string) writer.write(glyphs_object) return string.getvalue().splitlines() class AssertLinesEqual(object): def assertLinesEqual(self, expected, actual, message): if actual != expected: if len(actual) < len(expected): sys.stderr.write(dedent("""\ WARNING: the actual text is shorter that the expected text. Some information may be LOST! """)) for line in difflib.unified_diff( expected, actual, fromfile="", tofile=""): if not line.endswith("\n"): line += "\n" sys.stderr.write(line) self.fail(message) class AssertParseWriteRoundtrip(AssertLinesEqual): def assertParseWriteRoundtrip(self, filename): with open(filename) as f: expected = f.read().splitlines() f.seek(0, 0) font = glyphsLib.load(f) actual = write_to_lines(font) # Roundtrip again to check idempotence font = glyphsLib.loads("\n".join(actual)) actual_idempotent = write_to_lines(font) # Assert idempotence first, because if that fails it's a big issue self.assertLinesEqual( actual, actual_idempotent, "The parser/writer should be idempotent. BIG PROBLEM!") self.assertLinesEqual( expected, actual, "The writer should output exactly what the parser read") class AssertUFORoundtrip(AssertLinesEqual): def assertUFORoundtrip(self, font): expected = write_to_lines(font) roundtrip = to_glyphs(to_ufos(font)) actual = write_to_lines(roundtrip) self.assertLinesEqual( expected, actual, "The font has been modified by the roundtrip") glyphslib-2.2.1/tests/types_test.py000066400000000000000000000046441322341616200174400ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import ( print_function, division, absolute_import, unicode_literals) import datetime import unittest from glyphsLib.types import glyphs_datetime class GlyphsDateTimeTest(unittest.TestCase): def test_parsing_24hr_format(self): """Assert glyphs_datetime can parse 24 hour time formats""" string_24hrs = '2017-01-01 17:30:30 +0000' test_time = glyphs_datetime() self.assertEqual(test_time.read(string_24hrs), datetime.datetime(2017, 1, 1, 17, 30, 30)) def test_parsing_12hr_format(self): """Assert glyphs_datetime can parse 12 hour time format""" string_12hrs = '2017-01-01 5:30:30 PM +0000' test_time = glyphs_datetime() self.assertEqual(test_time.read(string_12hrs), datetime.datetime(2017, 1, 1, 17, 30, 30)) def test_parsing_timezone(self): """Assert glyphs_datetime can parse the (optional) timezone formatted as UTC offset. If it's not explicitly specified, then +0000 is assumed. """ self.assertEqual(glyphs_datetime().read('2017-12-18 16:45:31 -0100'), datetime.datetime(2017, 12, 18, 15, 45, 31)) self.assertEqual(glyphs_datetime().read('2017-12-18 14:15:31 +0130'), datetime.datetime(2017, 12, 18, 15, 45, 31)) self.assertEqual(glyphs_datetime().read('2017-12-18 15:45:31'), datetime.datetime(2017, 12, 18, 15, 45, 31)) self.assertEqual(glyphs_datetime().read('2017-12-18 03:45:31 PM'), datetime.datetime(2017, 12, 18, 15, 45, 31)) self.assertEqual(glyphs_datetime().read('2017-12-18 09:45:31 AM'), datetime.datetime(2017, 12, 18, 9, 45, 31)) if __name__ == '__main__': unittest.main() glyphslib-2.2.1/tests/writer_test.py000066400000000000000000001007551322341616200176100ustar00rootroot00000000000000# coding=UTF-8 # # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import unittest import math from textwrap import dedent from collections import OrderedDict import os from fontTools.misc.py23 import UnicodeIO import glyphsLib from glyphsLib import classes from glyphsLib.types import glyphs_datetime, point, rect from glyphsLib.writer import dump, dumps from glyphsLib.parser import Parser import test_helpers class WriterTest(unittest.TestCase, test_helpers.AssertLinesEqual): def assertWrites(self, glyphs_object, text): """Assert that the given object, when given to the writer, produces the given text. """ expected = text.splitlines() actual = test_helpers.write_to_lines(glyphs_object) self.assertLinesEqual( expected, actual, "The writer has not produced the expected output") def assertWritesValue(self, glyphs_value, text): """Assert that the writer produces the given text for the given value.""" expected = dedent("""\ {{ writtenValue = {0}; }} """).format(text).splitlines() # We wrap the value in a dict to use the same test helper actual = test_helpers.write_to_lines({'writtenValue': glyphs_value}) self.assertLinesEqual( expected, actual, "The writer has not produced the expected output") def test_write_font_attributes(self): """Test the writer on all GSFont attributes""" font = classes.GSFont() # List of properties from https://docu.glyphsapp.com/#gsfont # parent: not handled because it's internal and read-only # masters m1 = classes.GSFontMaster() m1.id = "M1" font.masters.insert(0, m1) m2 = classes.GSFontMaster() m2.id = "M2" font.masters.insert(1, m2) # instances i1 = classes.GSInstance() i1.name = "MuchBold" font.instances.append(i1) # glyphs g1 = classes.GSGlyph() g1.name = 'G1' font.glyphs.append(g1) # classes c1 = classes.GSClass() c1.name = "C1" font.classes.append(c1) # features f1 = classes.GSFeature() f1.name = "F1" font.features.append(f1) # featurePrefixes fp1 = classes.GSFeaturePrefix() fp1 = "FP1" font.featurePrefixes.append(fp1) # copyright font.copyright = "Copyright Bob" # designer font.designer = "Bob" # designerURL font.designerURL = "bob.me" # manufacturer font.manufacturer = "Manu" # manufacturerURL font.manufacturerURL = "manu.com" # versionMajor font.versionMajor = 2 # versionMinor font.versionMinor = 104 # date font.date = glyphs_datetime('2017-10-03 07:35:46 +0000') # familyName font.familyName = "Sans Rien" # upm font.upm = 2000 # note font.note = "Was bored, made this" # kerning font.kerning = OrderedDict([ ('M1', OrderedDict([ ('@MMK_L_G1', OrderedDict([ ('@MMK_R_G1', 0.1) ])) ])) ]) # userData font.userData = { 'a': 'test', 'b': [1, {'c': 2}], 'd': [1, "1"], } # grid -> gridLength font.grid = 35 # gridSubDivisions font.gridSubDivisions = 5 # keyboardIncrement font.keyboardIncrement = 1.2 # disablesNiceNames font.disablesNiceNames = True # customParameters font.customParameters['ascender'] = 300 # selection: not written # selectedLayers: not written # selectedFontMaster: not written # masterIndex: not written # currentText: not written # tabs: not written # currentTab: not written # filepath: not written # tool: not written # tools: not handled because it is a read-only list of GUI features # .appVersion (extra property that is not in the docs!) font.appVersion = "895" self.assertWrites(font, dedent("""\ { .appVersion = "895"; classes = ( { code = ""; name = C1; } ); copyright = "Copyright Bob"; customParameters = ( { name = note; value = "Was bored, made this"; }, { name = ascender; value = 300; } ); date = "2017-10-03 07:35:46 +0000"; designer = Bob; designerURL = bob.me; disablesNiceNames = 1; familyName = "Sans Rien"; featurePrefixes = ( FP1 ); features = ( { code = ""; name = F1; } ); fontMaster = ( { ascender = 800; capHeight = 700; id = M1; xHeight = 500; }, { ascender = 800; capHeight = 700; id = M2; xHeight = 500; } ); glyphs = ( { glyphname = G1; } ); gridLength = 35; gridSubDivision = 5; instances = ( { name = MuchBold; } ); kerning = { M1 = { "@MMK_L_G1" = { "@MMK_R_G1" = 0.1; }; }; }; keyboardIncrement = 1.2; manufacturer = Manu; manufacturerURL = manu.com; unitsPerEm = 2000; userData = { a = test; b = ( 1, { c = 2; } ); d = ( 1, "1" ); }; versionMajor = 2; versionMinor = 104; } """)) # Don't write the keyboardIncrement if it's 1 (default) font.keyboardIncrement = 1 written = test_helpers.write_to_lines(font) self.assertFalse(any("keyboardIncrement" in line for line in written)) def test_write_font_master_attributes(self): """Test the writer on all GSFontMaster attributes""" master = classes.GSFontMaster() # List of properties from https://docu.glyphsapp.com/#gsfontmaster # id master.id = "MASTER-ID" # name # Cannot set the `name` attribute directly # master.name = "Hairline Megawide" master.customParameters['Master Name'] = "Hairline Megawide" # weight master.weight = "Thin" # width master.width = "Wide" # weightValue master.weightValue = 0.01 # widthValue master.widthValue = 0.99 # customValue # customName master.customName = "cuteness" # A value of 0.0 is not written to the file. master.customValue = 0.001 master.customName1 = "color" master.customValue1 = 0.1 master.customName2 = "depth" master.customValue2 = 0.2 master.customName3 = "surealism" master.customValue3 = 0.3 # ascender master.ascender = 234.5 # capHeight master.capHeight = 200.6 # xHeight master.xHeight = 59.1 # descender master.descender = -89.2 # italicAngle master.italicAngle = 12.2 # verticalStems master.verticalStems = [1, 2, 3] # horizontalStems master.horizontalStems = [4, 5, 6] # alignmentZones zone = classes.GSAlignmentZone(0, -30) master.alignmentZones = [ zone ] # blueValues: not handled because it is read-only # otherBlues: not handled because it is read-only # guides guide = classes.GSGuideLine() guide.name = "middle" master.guides.append(guide) # userData master.userData['rememberToMakeTea'] = True # customParameters master.customParameters['underlinePosition'] = -135 self.assertWrites(master, dedent("""\ { alignmentZones = ( "{0, -30}" ); ascender = 234.5; capHeight = 200.6; custom = cuteness; customValue = 0.001; custom1 = color; customValue1 = 0.1; custom2 = depth; customValue2 = 0.2; custom3 = surealism; customValue3 = 0.3; customParameters = ( { name = "Master Name"; value = "Hairline Megawide"; }, { name = underlinePosition; value = -135; } ); descender = -89.2; guideLines = ( { name = middle; } ); horizontalStems = ( 4, 5, 6 ); id = "MASTER-ID"; italicAngle = 12.2; name = "Hairline Megawide"; userData = { rememberToMakeTea = 1; }; verticalStems = ( 1, 2, 3 ); weight = Thin; weightValue = 0.01; width = Wide; widthValue = 0.99; xHeight = 59.1; } """)) # Write the capHeight and xHeight even if they are "0" master.xHeight = 0 master.capHeight = 0 written = test_helpers.write_to_lines(master) self.assertIn("xHeight = 0;", written) self.assertIn("capHeight = 0;", written) def test_write_alignment_zone(self): zone = classes.GSAlignmentZone(23, 40) self.assertWritesValue(zone, '"{23, 40}"') def test_write_instance(self): instance = classes.GSInstance() # List of properties from https://docu.glyphsapp.com/#gsinstance # active # FIXME: (jany) does not seem to be handled by this library? No doc? instance.active = True # name instance.name = "SemiBoldCompressed (name)" # weight instance.weight = "SemiBold (weight)" # width instance.width = "Compressed (width)" # weightValue instance.weightValue = 600 # widthValue instance.widthValue = 200 # customValue instance.customValue = 0.4 # isItalic instance.isItalic = True # isBold instance.isBold = True # linkStyle instance.linkStyle = "linked style value" # familyName instance.familyName = "Sans Rien (familyName)" # preferredFamily instance.preferredFamily = "Sans Rien (preferredFamily)" # preferredSubfamilyName instance.preferredSubfamilyName = "Semi Bold Compressed (preferredSubFamilyName)" # windowsFamily instance.windowsFamily = "Sans Rien MS (windowsFamily)" # windowsStyle: read only # windowsLinkedToStyle: read only # fontName instance.fontName = "SansRien (fontName)" # fullName instance.fullName = "Sans Rien Semi Bold Compressed (fullName)" # customParameters instance.customParameters['hheaLineGap'] = 10 # instanceInterpolations instance.instanceInterpolations = { 'M1': 0.2, 'M2': 0.8 } # manualInterpolation instance.manualInterpolation = True # interpolatedFont: read only # FIXME: (jany) the weight and width are not in the output # confusion with weightClass/widthClass? self.assertWrites(instance, dedent("""\ { customParameters = ( { name = famiyName; value = "Sans Rien (familyName)"; }, { name = preferredFamily; value = "Sans Rien (preferredFamily)"; }, { name = preferredSubfamilyName; value = "Semi Bold Compressed (preferredSubFamilyName)"; }, { name = styleMapFamilyName; value = "Sans Rien MS (windowsFamily)"; }, { name = postscriptFontName; value = "SansRien (fontName)"; }, { name = postscriptFullName; value = "Sans Rien Semi Bold Compressed (fullName)"; }, { name = hheaLineGap; value = 10; } ); interpolationCustom = 0.4; interpolationWeight = 600; interpolationWidth = 200; instanceInterpolations = { M1 = 0.2; M2 = 0.8; }; isBold = 1; isItalic = 1; linkStyle = "linked style value"; manualInterpolation = 1; name = "SemiBoldCompressed (name)"; } """)) def test_write_custom_parameter(self): # Name without quotes self.assertWritesValue( classes.GSCustomParameter('myParam', 'myValue'), "{\nname = myParam;\nvalue = myValue;\n}") # Name with quotes self.assertWritesValue( classes.GSCustomParameter('my param', 'myValue'), "{\nname = \"my param\";\nvalue = myValue;\n}") # Value with quotes self.assertWritesValue( classes.GSCustomParameter('myParam', 'my value'), "{\nname = myParam;\nvalue = \"my value\";\n}") # Int param (ascender): should convert the value to string self.assertWritesValue( classes.GSCustomParameter('ascender', 12), "{\nname = ascender;\nvalue = 12;\n}") # Float param (postscriptBlueScale): should convert the value to string self.assertWritesValue( classes.GSCustomParameter('postscriptBlueScale', 0.125), "{\nname = postscriptBlueScale;\nvalue = 0.125;\n}") # Bool param (isFixedPitch): should convert the boolean value to 0/1 self.assertWritesValue( classes.GSCustomParameter('isFixedPitch', True), "{\nname = isFixedPitch;\nvalue = 1;\n}") # Intlist param: should map list of int to list of strings self.assertWritesValue( classes.GSCustomParameter('fsType', [1, 2]), "{\nname = fsType;\nvalue = (\n1,\n2\n);\n}") def test_write_class(self): class_ = classes.GSClass() class_.name = "e" class_.code = "e eacute egrave" class_.automatic = True self.assertWrites(class_, dedent("""\ { automatic = 1; code = "e eacute egrave"; name = e; } """)) # When the code is an empty string, write an empty string class_.code = "" self.assertWrites(class_, dedent("""\ { automatic = 1; code = ""; name = e; } """)) def test_write_feature_prefix(self): fp = classes.GSFeaturePrefix() fp.name = "Languagesystems" fp.code = "languagesystem DFLT dflt;" fp.automatic = True self.assertWrites(fp, dedent("""\ { automatic = 1; code = "languagesystem DFLT dflt;"; name = Languagesystems; } """)) def test_write_feature(self): feature = classes.GSFeature() feature.name = "sups" feature.code = " sub @standard by @sups;" feature.automatic = True feature.notes = "notes about sups" self.assertWrites(feature, dedent("""\ { automatic = 1; code = " sub @standard by @sups;"; name = sups; notes = "notes about sups"; } """)) def test_write_glyph(self): glyph = classes.GSGlyph() # https://docu.glyphsapp.com/#gsglyph # parent: not written # layers # Put the glyph in a font with at least one master for the magic in # `glyph.layers.append()` to work. font = classes.GSFont() master = classes.GSFontMaster() master.id = "MASTER-ID" font.masters.insert(0, master) font.glyphs.append(glyph) layer = classes.GSLayer() layer.layerId = "LAYER-ID" layer.name = "L1" glyph.layers.insert(0, layer) # name glyph.name = "Aacute" # unicode glyph.unicode = "00C1" # string: not written # id: not written # category glyph.category = "Letter" # subCategory glyph.subCategory = "Uppercase" # script glyph.script = "latin" # productionName glyph.productionName = "Aacute.prod" # glyphInfo: not written # leftKerningGroup glyph.leftKerningGroup = "A" # rightKerningGroup glyph.rightKerningGroup = "A" # leftKerningKey: not written # rightKerningKey: not written # leftMetricsKey glyph.leftMetricsKey = "A" # rightMetricsKey glyph.rightMetricsKey = "A" # widthMetricsKey glyph.widthMetricsKey = "A" # export glyph.export = False # color glyph.color = 11 # colorObject: not written # note glyph.note = "Stunning one-bedroom A with renovated acute accent" # selected: not written # mastersCompatible: not stored # userData glyph.userData['rememberToMakeCoffe'] = True # Check that empty collections are written glyph.userData['com.someoneelse.coolsoftware.customdata'] = [ OrderedDict([ ('zero', 0), ('emptyList', []), ('emptyDict', {}), ('emptyString', ""), ]), [], {}, "", "hey", 0, 1, ] # smartComponentAxes axis = classes.GSSmartComponentAxis() axis.name = "crotchDepth" glyph.smartComponentAxes.append(axis) # lastChange glyph.lastChange = glyphs_datetime('2017-10-03 07:35:46 +0000') self.assertWrites(glyph, dedent("""\ { color = 11; export = 0; glyphname = Aacute; lastChange = "2017-10-03 07:35:46 +0000"; layers = ( { associatedMasterId = "MASTER-ID"; layerId = "LAYER-ID"; name = L1; width = 0; } ); leftKerningGroup = A; leftMetricsKey = A; widthMetricsKey = A; note = "Stunning one-bedroom A with renovated acute accent"; rightKerningGroup = A; rightMetricsKey = A; unicode = 00C1; script = latin; category = Letter; subCategory = Uppercase; userData = { com.someoneelse.coolsoftware.customdata = ( { zero = 0; emptyList = ( ); emptyDict = { }; emptyString = ""; }, ( ), { }, "", hey, 0, 1 ); rememberToMakeCoffe = 1; }; partsSettings = ( { name = crotchDepth; bottomValue = 0; topValue = 0; } ); } """)) # Write the script even when it's an empty string # Same for category and subCategory glyph.script = "" glyph.category = "" glyph.subCategory = "" written = test_helpers.write_to_lines(glyph) self.assertIn('script = "";', written) self.assertIn('category = "";', written) self.assertIn('subCategory = "";', written) def test_write_layer(self): layer = classes.GSLayer() # http://docu.glyphsapp.com/#gslayer # parent: not written # name layer.name = '{125, 100}' # associatedMasterId layer.associatedMasterId = 'M1' # layerId layer.layerId = 'L1' # color layer.color = 2 # brown # colorObject: read-only, computed # components component = classes.GSComponent(glyph='glyphName') layer.components.append(component) # guides guide = classes.GSGuideLine() guide.name = 'xheight' layer.guides.append(guide) # annotations annotation = classes.GSAnnotation() annotation.type = classes.TEXT annotation.text = 'Fuck, this curve is ugly!' layer.annotations.append(annotation) # hints hint = classes.GSHint() hint.name = 'hintName' layer.hints.append(hint) # anchors anchor = classes.GSAnchor() anchor.name = 'top' layer.anchors['top'] = anchor # paths path = classes.GSPath() layer.paths.append(path) # selection: read-only # LSB, RSB, TSB, BSB: not written # width layer.width = 890.4 # leftMetricsKey layer.leftMetricsKey = "A" # rightMetricsKey layer.rightMetricsKey = "A" # widthMetricsKey layer.widthMetricsKey = "A" # bounds: read-only, computed # selectionBounds: read-only, computed # background # FIXME: (jany) why not use a GSLayer like the official doc suggests? background_layer = classes.GSBackgroundLayer() layer.background = background_layer # backgroundImage image = classes.GSBackgroundImage('/path/to/file.jpg') layer.backgroundImage = image # bezierPath: read-only, objective-c # openBezierPath: read-only, objective-c # completeOpenBezierPath: read-only, objective-c # isAligned # FIXME: (jany) is this read-only? # is this computed from each component's alignment? # layer.isAligned = False # userData layer.userData['rememberToMakeCoffe'] = True # smartComponentPoleMapping layer.smartComponentPoleMapping['crotchDepth'] = 2 # Top pole layer.smartComponentPoleMapping['shoulderWidth'] = 1 # Bottom pole self.assertWrites(layer, dedent("""\ { anchors = ( { name = top; } ); annotations = ( { position = ; text = "Fuck, this curve is ugly!"; type = 1; } ); associatedMasterId = M1; background = { }; backgroundImage = { crop = "{{0, 0}, {0, 0}}"; imagePath = "/path/to/file.jpg"; }; color = 2; components = ( { name = glyphName; } ); guideLines = ( { name = xheight; } ); hints = ( { name = hintName; } ); layerId = L1; leftMetricsKey = A; widthMetricsKey = A; rightMetricsKey = A; name = "{125, 100}"; paths = ( { } ); userData = { PartSelection = { crotchDepth = 2; shoulderWidth = 1; }; rememberToMakeCoffe = 1; }; width = 890.4; } """)) # Don't write a blank layer name layer.name = "" written = test_helpers.write_to_lines(layer) self.assertNotIn('name = "";', written) def test_write_anchor(self): anchor = classes.GSAnchor('top', point(23, 45.5)) self.assertWrites(anchor, dedent("""\ { name = top; position = "{23, 45.5}"; } """)) def test_write_component(self): component = classes.GSComponent("dieresis") # http://docu.glyphsapp.com/#gscomponent # position component.position = point(45.5, 250) # scale component.scale = 2.0 # rotation component.rotation = 90 # componentName: already set at init # component: read-only # layer: read-only # transform: already set using scale & position # bounds: read-only, objective-c # automaticAlignment component.automaticAlignment = True # anchor component.anchor = "top" # selected: not written # smartComponentValues component.smartComponentValues = { "crotchDepth": -77, } # bezierPath: read-only, objective-c self.assertWrites(component, dedent("""\ { anchor = top; name = dieresis; piece = { crotchDepth = -77; }; transform = "{0, 2, -2, 0, 45.5, 250}"; } """)) def test_write_smart_component_axis(self): axis = classes.GSSmartComponentAxis() # http://docu.glyphsapp.com/#gssmartcomponentaxis axis.name = "crotchDepth" axis.topName = "High" axis.topValue = 0 axis.bottomName = "Low" axis.bottomValue = -100 self.assertWrites(axis, dedent("""\ { name = crotchDepth; bottomName = Low; bottomValue = -100; topName = High; topValue = 0; } """)) def test_write_path(self): path = classes.GSPath() # http://docu.glyphsapp.com/#gspath # parent: not written # nodes node = classes.GSNode() path.nodes.append(node) # segments: computed, objective-c # closed path.closed = True # direction: computed # bounds: computed # selected: not written # bezierPath: computed self.assertWrites(path, dedent("""\ { closed = 1; nodes = ( "0 0 LINE" ); } """)) def test_write_node(self): node = classes.GSNode(point(10, 30), classes.GSNode.CURVE) # http://docu.glyphsapp.com/#gsnode # position: already set # type: already set # smooth node.smooth = True # connection: deprecated # selected: not written # index, nextNode, prevNode: computed # name node.name = "top-left corner" # userData node.userData["rememberToDownloadARealRemindersApp"] = True self.assertWritesValue( node, '"10 30 CURVE SMOOTH {name = \\"top-left corner\\";\\n\ rememberToDownloadARealRemindersApp = 1;}"' ) # Write floating point coordinates node = classes.GSNode(point(499.99, 512.01), classes.GSNode.OFFCURVE) self.assertWritesValue( node, '"499.99 512.01 OFFCURVE"' ) # Write userData with special characters test_user_data = { '\nkey"\';\n\n\n': '"\'value\nseveral lines\n;\n', ';': ';\n', 'escapeception': '\\"\\\'\\n\\\\n', } node = classes.GSNode(point(130, 431), classes.GSNode.LINE) for key, value in test_user_data.items(): node.userData[key] = value # This is the output of Glyphs 1089 expected_output = '"130 431 LINE {\\"\\012key\\\\"\';\\012\\012\\012\\" = \\"\\\\"\'value\\012several lines\\012;\\012\\";\\n\\";\\" = \\";\\012\\";\\nescapeception = \\"\\\\\\\\"\\\\\'\\\\n\\\\\\\\n\\";}"' self.assertWritesValue(node, expected_output) # Check that we can read the userData back node = Parser(classes.GSNode).parse(expected_output) self.assertEqual(test_user_data, dict(node.userData)) def test_write_guideline(self): line = classes.GSGuideLine() # http://docu.glyphsapp.com/#GSGuideLine line.position = point(56, 45) line.angle = 11.0 line.name = "italic angle" # selected: not written self.assertWrites(line, dedent("""\ { angle = 11; name = "italic angle"; position = "{56, 45}"; } """)) def test_write_annotation(self): annotation = classes.GSAnnotation() # http://docu.glyphsapp.com/#gsannotation annotation.position = point(12, 34) annotation.type = classes.TEXT annotation.text = "Look here" annotation.angle = 123.5 annotation.width = 135 self.assertWrites(annotation, dedent("""\ { angle = 123.5; position = "{12, 34}"; text = "Look here"; type = 1; width = 135; } """)) def test_write_hint(self): hint = classes.GSHint() # http://docu.glyphsapp.com/#gshint layer = classes.GSLayer() path1 = classes.GSPath() layer.paths.append(path1) node1 = classes.GSNode(point(100, 100)) path1.nodes.append(node1) hint.originNode = node1 node2 = classes.GSNode(point(200, 200)) path1.nodes.append(node2) hint.targetNode = node2 node3 = classes.GSNode(point(300, 300)) path1.nodes.append(node3) hint.otherNode1 = node3 path2 = classes.GSPath() layer.paths.append(path2) node4 = classes.GSNode(point(400, 400)) path2.nodes.append(node4) hint.otherNode2 = node4 hint.type = classes.CORNER hint.options = classes.TTROUND | classes.TRIPLE hint.horizontal = True # selected: not written hint.name = "My favourite hint" self.assertWrites(hint, dedent("""\ { horizontal = 1; origin = "{0, 0}"; target = "{0, 1}"; other1 = "{0, 2}"; other2 = "{1, 0}"; type = 16; name = "My favourite hint"; options = 128; } """)) # FIXME: (jany) What about the undocumented scale & stem? # -> Add a test for that # Test with target = "up" # FIXME: (jany) what does target = "up" mean? # Is there an official python API to write that? # hint.targetNode = 'up' # written = test_helpers.write_to_lines(hint) # self.assertIn('target = up;', written) def test_write_background_image(self): image = classes.GSBackgroundImage('/tmp/img.jpg') # http://docu.glyphsapp.com/#gsbackgroundimage # path: already set # image: read-only, objective-c image.crop = rect(point(0, 10), point(500, 510)) image.locked = True image.alpha = 70 image.position = point(40, 90) image.scale = (1.1, 1.2) image.rotation = 0.3 # transform: Already set with scale/rotation self.assertWrites(image, dedent("""\ { alpha = 70; crop = "{{0, 10}, {500, 510}}"; imagePath = "/tmp/img.jpg"; locked = 1; transform = ( 1.09998, 0.00576, -0.00628, 1.19998, 40, 90 ); } """)) class WriterDumpInterfaceTest(unittest.TestCase): def test_dump(self): obj = classes.GSFont() fp = UnicodeIO() dump(obj, fp) self.assertTrue(fp.getvalue()) def test_dumps(self): obj = classes.GSFont() string = dumps(obj) self.assertTrue(string) class WriterRoundtripTest(unittest.TestCase, test_helpers.AssertParseWriteRoundtrip): def test_roundtrip_on_file(self): filename = os.path.join( os.path.dirname(__file__), 'data/GlyphsUnitTestSans.glyphs') self.assertParseWriteRoundtrip(filename) if __name__ == '__main__': unittest.main() glyphslib-2.2.1/tox.ini000066400000000000000000000010041322341616200150170ustar00rootroot00000000000000[tox] envlist = py27, py36, htmlcov [testenv] deps = pytest coverage py27: mock>=2.0.0 -rrequirements.txt commands = coverage run --parallel-mode -m pytest {posargs} [testenv:htmlcov] basepython = python3.6 deps = coverage skip_install = true commands = coverage combine coverage report coverage html [testenv:codecov] passenv = * deps = coverage codecov skip_install = true ignore_outcome = true commands = coverage combine codecov --env TRAVIS_PYTHON_VERSION