pax_global_header00006660000000000000000000000064147753312520014523gustar00rootroot0000000000000052 comment=26e8b8cae9ae7b5ff09fbd99139eeced78be4c42 robotools-fontParts-26e8b8c/000077500000000000000000000000001477533125200161465ustar00rootroot00000000000000robotools-fontParts-26e8b8c/.codecov.yml000066400000000000000000000002131477533125200203650ustar00rootroot00000000000000comment: layout: "diff, files" behavior: default require_changes: true coverage: status: project: off patch: off robotools-fontParts-26e8b8c/.coveragerc000066400000000000000000000015571477533125200202770ustar00rootroot00000000000000[run] # measure 'branch' coverage in addition to 'statement' coverage # See: http://coverage.readthedocs.io/en/coverage-4.5.1/branch.html branch = True # list of directories or packages to measure source = fontParts omit = */__init__.py */test/* */test.py [paths] source = Lib/fontParts .tox/*/lib/python*/site-packages/fontParts .tox/pypy*/site-packages/fontParts [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|except)(\s)?NotImplementedError # don't complain if non-runnable code isn't run if __name__ == .__main__.: # ignore source code that can’t be found ignore_errors = True # when running a summary report, show missing lines show_missing = True robotools-fontParts-26e8b8c/.github/000077500000000000000000000000001477533125200175065ustar00rootroot00000000000000robotools-fontParts-26e8b8c/.github/workflows/000077500000000000000000000000001477533125200215435ustar00rootroot00000000000000robotools-fontParts-26e8b8c/.github/workflows/publish-package.yml000066400000000000000000000017401477533125200253270ustar00rootroot00000000000000name: Build and Publish Python Package on: push: tags: - '[0-9].*' jobs: create_release: name: Create GitHub Release runs-on: ubuntu-latest steps: - name: Create release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: false prerelease: true deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3.5.3 - name: Set up Python uses: actions/setup-python@v4.7.0 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* robotools-fontParts-26e8b8c/.github/workflows/run-tests.yml000066400000000000000000000031761477533125200242410ustar00rootroot00000000000000name: Tests on: push: branches: [master] pull_request: branches: [master] workflow_dispatch: inputs: reason: description: 'Reason for running workflow' required: true jobs: test: runs-on: ${{ matrix.platform }} if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] platform: [ubuntu-latest, macos-latest, windows-latest] exclude: # Only test on the oldest and latest supported stable Python on macOS and Windows. - platform: macos-latest python-version: 3.10 - platform: windows-latest python-version: 3.10 - platform: macos-latest python-version: 3.11 - platform: windows-latest python-version: 3.11 - platform: macos-latest python-version: 3.13 - platform: windows-latest python-version: 3.13 steps: - uses: actions/checkout@v3.5.3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4.7.0 with: python-version: ${{ matrix.python-version }} - name: Install packages run: pip install tox coverage - name: Run Tox run: tox -e py-cov - name: Produce coverage files run: | coverage combine coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: file: coverage.xml flags: unittests name: codecov-umbrella fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} robotools-fontParts-26e8b8c/.gitignore000066400000000000000000000004541477533125200201410ustar00rootroot00000000000000.cache .tox .coverage .coverage.* .DS_Store *.egg-info __pycache__/ *.py[cod] *.pyc build/* dist/* .eggs/ .vscode documentation/build/* documentation/.sass-cache/* documentation/source/_themes/fontPartsTheme/static/.sass-cache/* doc/* .pytest_cache htmlcov # scm version Lib/fontParts/_version.pyrobotools-fontParts-26e8b8c/.prospector.yaml000066400000000000000000000002411477533125200213050ustar00rootroot00000000000000strictness: high pylint: run: false # since Codacy runs pylint in addition to prospector pep8: options: single-line-if-stmt: n disable: - N802 robotools-fontParts-26e8b8c/.readthedocs.yaml000066400000000000000000000005711477533125200214000ustar00rootroot00000000000000# Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3" sphinx: configuration: documentation/source/conf.py python: install: - requirements: requirements.txtrobotools-fontParts-26e8b8c/CONTRIBUTING.rst000066400000000000000000000271231477533125200206140ustar00rootroot00000000000000========================= Contributing to fontParts ========================= Thanks for being interested in helping out with fontParts, we really appreciate it! 🎉👍 Below is a short guide to what we need help with, where to find both documentation about helping, and where to find examples to model when writing code or documentation. Like anything in fontParts, if you see something that needs improvement/isn’t clear/could be added to, you can submit pull requests for this document. **Right now we need the most help with writing tests and writing documentation.** ----------------- Table of contents ----------------- 1. `Contributing with Issues`_ 2. `I just have a question!`_ 3. `Contributing Tests`_ a) `What should I install to write tests?`_ b) `How do I know what tests to write?`_ 4. `Contributing Documentation`_ 5. `Creating a fork & a pull request`_ a) `Creating a fork`_ b) `Creating a branch`_ c) `Keeping your working branch in sync`_ d) `Making a pull request`_ 6. `Style and other notes`_ ------------------------ Contributing with Issues ------------------------ There are three ways you can help with `issues `_. #. **Opening an issue for discussion.** Have you hit a bug? Does something not make sense? Is there a feature you would like so see? Open an `issue `_! #. **Helping out figuring out issues.** If you have experience with an issue, you can contribute further test cases or domain knowledge to the discussion to help move the issue along. #. **Fixing the issue.** Either show that the issue isn’t a problem or contribute a pull request that fixes the issue. One can view all the issues that we’d love help with by searching for `Help Wanted `_ in the issues. ----------------------- I just have a question! ----------------------- If you have a question about fontParts that doesn’t make sense as an issue, then tweeting to `@fontparts `_ will get an answer. Please read the documentation first to see if your question is answered (as the documentation is still being written, it may not be). ------------------ Contributing Tests ------------------ Oh man, thank you! Writing test cases is like kerning, sometimes it’s really soothing, sometimes it feels like it might just not end. Any test case that you want to contribute is one less that someone needs to write, so we really thank you for your interest here. To start, you’ll see a list of all that needs to be done on the `Tests Project `_ on GitHub. Each object that needs tests has an issue where one can discuss the tests, ask questions about what should be done, and (hopefully) where you will volunteer to take ownership of writing tests for that object. If you want to take ownership for testing an object, just say so on the issue and we’ll add you as a contributor and assign that issue to you. Our tests are written with Python’s unittest framework, if you’ve not used it before, browsing the `documentation for unittest `_ will be helpful to understand what’s going on. How to write tests for fontParts is covered in the fontParts documentation, in `Testing `_. A good place to start looking to see examples for how to write the tests is in the `test_glyph.py `_ and `test_component.py `_ files. What should I install to write tests? ------------------------------------- Having both ``tox`` and ``coverage`` installed locally are great aids to writing tests. `Tox `_ is installed via: :: pip install tox Once ``tox`` is installed, you can run the tests for fontParts by just typing ``tox`` on the command line when you are in the fontParts directory. ``tox`` is set up to test fontParts in Python 2.7, 3.5, 3.6, and PyPy. It’s likely that you don’t have all of those versions installed on your machine (looking at you pypy). Don’t worry about testing errors for python versions that aren’t installed. If you don’t have a version of Python 3 installed, you should install version 3.6. On the MacOS, it’s easiest to do via `Homebrew `_, but whatever you are most comfortable with will likely be OK. `Coverage `_ is installed via: :: pip install coverage After installing it, run: :: coverage run Lib/fontParts/fontshell/test.py coverage html And a folder named **htmlcov** containing a bunch of files will be created. Open the file named **index.html** in that folder. This will allow you to get an update of the coverage before you push out a commit. You can also check if the tests run on Python 3 by using ``coverage3`` instead of ``coverage`` (the former invokes ``python3`` whereas the latter calls up ``python``). ``tox`` is also set up to run the coverage tests, so if you have Python 3.6 installed, each time you run ``tox`` it will update the **htmlcov** folder for you. **Note:** Coverage is great for showing what lines of code may be missed, and is a good yardstick to measure your progress. However, it can’t and doesn’t know everything that may go wrong, so you will need to think about the object you are writing tests for and have a logical plan for what might go wrong & what to then test. How do I know what tests to write? ---------------------------------- Check the `Codecov page for fontParts base `_ to see which lines of the code are still not being hit by tests. Because the automated tests are run in ``fontshell``, a good starting point for writing tests is to get coverage to 100% on the `fontshell version `_ of the object, and then determine what other tests need to be added to get 100% on the base files. Do not worry about testing ``repr``. -------------------------- Contributing Documentation -------------------------- We want fontParts to have clear, easy to follow documentation. This library is aimed at working typeface designers who want to script some of their work flow. Easy to follow documentation is a big part of making that as pleasant and easy as possible. Like Tests, there is a `Documentation `_ project on GitHub with a bunch of issues for specific things that need to be written. Each issue is where you can ask questions about writing documentation for that thing and hopefully volunteer to be in charge of writing the documentation for that issue. If you want to take ownership for writing a piece of the documentation, just say so on the issue and we’ll add you as a contributor and assign that issue to you. There are four types of things that we need help with in the documentation: #. **Design**. We’d love to have the standard Sphinx CSS redone. This is a great opportunity for someone who is handy with HTML/CSS. We do have a logo that is forthcoming. #. **Writing introduction**. RoboFab had a bunch of really good introductory documentation that we want to port over. #. **Writing object documentation**. The main part of the documentation happens in the code for each object. This is nearly done, but there are several objects that currently don’t have full documentation. #. **Checking written documentation**. We need to double check the Object documentation that has been written to be sure what we didn’t later add a method/attribute that needs documentation. Our documentation is written with reStructeredText markup. The `Quick reStructredText Primer `_ is a good reference to the markup style. fontParts has a `style guide and howto `_ for documentation, before starting please give it a read. A good example of the Object documentation can be found in the `Glyph `_ object. -------------------------------- Creating a fork & a pull request -------------------------------- Wait, “pull request”?! Don’t worry, though it can be a bit confusing at the start, changes to the code should be made via pull requests on the GitHub repository for fontParts. To do so, you’ll first need a GitHub account. If you don’t have one, you can `sign up `_ for one for free. Creating a fork --------------- Once you have a GitHub account, you’ll want to fork the project `on GitHub `_ and then clone your fork locally. Do so on the command line with: :: git clone git@github.com:username/fontParts.git cd fontParts git remote add upstream https://github.com/robofab-developers/fontParts.git git fetch upstream After doing this, it’s a good idea to at least install `tox `_ for local testing. See “`What should I install to write tests?`_” for how to install ``tox``. Creating a branch ----------------- Once you have your fork set up, it’s time to make changes to the code or documentation. To do so, create a branch of the code for the work you’re about to do. This is done by typing the following on the command line. (note: **my-branch** should be a logical name for the code that you want to change) :: git checkout -b my-branch -t upstream/master Make your changes, committing to your branch as things make sense. Keep your commit messages descriptive and as short as is needed to describe your changes. Keeping your working branch in sync ----------------------------------- As you work, it’s a good idea to “rebase” your branch after a commit to keep the bits that you aren’t changing in sync with the main repository. You do this by typing the following on the command line :: git fetch upstream git rebase upstream/master Making a pull request --------------------- Once you are done with your changes, you can create a pull request to merge your changes into fontParts. You do this by first pushing your working code to your fork on GitHub. This is done with (note: **my-branch** should be whatever you named your branch, not **my-branch**) :: git push origin my-branch Then on GitHub you’ll open a pull request (`more info `_). Please make your description of the pull request easy to understand. You may receive feedback on your pull request. As you make changes to the code based on the feedback, after you commit those changes locally, do the following on the command line to add the new changes to your pull request and GitHub will take care of the rest. :: git push origin my-branch After your pull request is accepted, you can delete your branch with :: git branch -d my-branch After doing so, it’s a good idea to then pull from the main repository to be sure that you have all the updated code :: git pull --------------------- Style and other notes --------------------- The style guide and other notes about developing fontParts is found `here `_ in the documentation. robotools-fontParts-26e8b8c/LICENSE000066400000000000000000000021011477533125200171450ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 The RoboFab Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. robotools-fontParts-26e8b8c/Lib/000077500000000000000000000000001477533125200166545ustar00rootroot00000000000000robotools-fontParts-26e8b8c/Lib/fontParts/000077500000000000000000000000001477533125200206345ustar00rootroot00000000000000robotools-fontParts-26e8b8c/Lib/fontParts/__init__.py000066400000000000000000000003231477533125200227430ustar00rootroot00000000000000try: from ._version import __version__ except ImportError: try: from setuptools_scm import get_version __version__ = get_version() except ImportError: __version__ = 'unknown' robotools-fontParts-26e8b8c/Lib/fontParts/base/000077500000000000000000000000001477533125200215465ustar00rootroot00000000000000robotools-fontParts-26e8b8c/Lib/fontParts/base/__init__.py000066400000000000000000000014011477533125200236530ustar00rootroot00000000000000from fontParts.base.errors import FontPartsError from fontParts.base.font import BaseFont from fontParts.base.info import BaseInfo from fontParts.base.groups import BaseGroups from fontParts.base.kerning import BaseKerning from fontParts.base.features import BaseFeatures from fontParts.base.lib import BaseLib from fontParts.base.layer import BaseLayer from fontParts.base.glyph import BaseGlyph from fontParts.base.contour import BaseContour from fontParts.base.point import BasePoint from fontParts.base.segment import BaseSegment from fontParts.base.bPoint import BaseBPoint from fontParts.base.component import BaseComponent from fontParts.base.anchor import BaseAnchor from fontParts.base.guideline import BaseGuideline from fontParts.base.image import BaseImage robotools-fontParts-26e8b8c/Lib/fontParts/base/anchor.py000066400000000000000000000235061477533125200234000ustar00rootroot00000000000000from fontTools.misc import transform from fontParts.base import normalizers from fontParts.base.base import ( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, PointPositionMixin, IdentifierMixin, dynamicProperty, reference ) from fontParts.base.compatibility import AnchorCompatibilityReporter from fontParts.base.color import Color from fontParts.base.deprecated import DeprecatedAnchor, RemovedAnchor class BaseAnchor( BaseObject, TransformationMixin, DeprecatedAnchor, RemovedAnchor, PointPositionMixin, InterpolationMixin, SelectionMixin, IdentifierMixin ): """ An anchor object. This object is almost always created with :meth:`BaseGlyph.appendAnchor`. An orphan anchor can be created like this:: >>> anchor = RAnchor() """ def _reprContents(self): contents = [ ("({x}, {y})".format(x=self.x, y=self.y)), ] if self.name is not None: contents.append("name='%s'" % self.name) if self.color: contents.append("color=%r" % str(self.color)) return contents # ---- # Copy # ---- copyAttributes = ( "x", "y", "name", "color" ) # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The anchor's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._glyph is not None: raise AssertionError("glyph for anchor already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Layer layer = dynamicProperty("layer", "The anchor's parent :class:`BaseLayer`.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The anchor's parent :class:`BaseFont`.") def _get_font(self): if self._glyph is None: return None return self.glyph.font # -------- # Position # -------- # x x = dynamicProperty( "base_x", """ The x coordinate of the anchor. It must be an :ref:`type-int-float`. :: >>> anchor.x 100 >>> anchor.x = 101 """ ) def _get_base_x(self): value = self._get_x() value = normalizers.normalizeX(value) return value def _set_base_x(self, value): value = normalizers.normalizeX(value) self._set_x(value) def _get_x(self): """ This is the environment implementation of :attr:`BaseAnchor.x`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_x(self, value): """ This is the environment implementation of :attr:`BaseAnchor.x`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # y y = dynamicProperty( "base_y", """ The y coordinate of the anchor. It must be an :ref:`type-int-float`. :: >>> anchor.y 100 >>> anchor.y = 101 """ ) def _get_base_y(self): value = self._get_y() value = normalizers.normalizeY(value) return value def _set_base_y(self, value): value = normalizers.normalizeY(value) self._set_y(value) def _get_y(self): """ This is the environment implementation of :attr:`BaseAnchor.y`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_y(self, value): """ This is the environment implementation of :attr:`BaseAnchor.y`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Identification # -------------- # index index = dynamicProperty( "base_index", """ The index of the anchor within the ordered list of the parent glyph's anchors. This attribute is read only. :: >>> anchor.index 0 """ ) def _get_base_index(self): value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Get the anchor's index. This must return an ``int``. Subclasses may override this method. """ glyph = self.glyph if glyph is None: return None return glyph.anchors.index(self) # name name = dynamicProperty( "base_name", """ The name of the anchor. This will be a :ref:`type-string` or ``None``. >>> anchor.name 'my anchor' >>> anchor.name = None """ ) def _get_base_name(self): value = self._get_name() value = normalizers.normalizeAnchorName(value) return value def _set_base_name(self, value): value = normalizers.normalizeAnchorName(value) self._set_name(value) def _get_name(self): """ This is the environment implementation of :attr:`BaseAnchor.name`. This must return a :ref:`type-string` or ``None``. The returned value will be normalized with :func:`normalizers.normalizeAnchorName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value): """ This is the environment implementation of :attr:`BaseAnchor.name`. **value** will be a :ref:`type-string` or ``None``. It will have been normalized with :func:`normalizers.normalizeAnchorName`. Subclasses must override this method. """ self.raiseNotImplementedError() # color color = dynamicProperty( "base_color", """ The anchor's color. This will be a :ref:`type-color` or ``None``. :: >>> anchor.color None >>> anchor.color = (1, 0, 0, 0.5) """ ) def _get_base_color(self): value = self._get_color() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_color(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_color(value) def _get_color(self): """ This is the environment implementation of :attr:`BaseAnchor.color`. This must return a :ref:`type-color` or ``None``. The returned value will be normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_color(self, value): """ This is the environment implementation of :attr:`BaseAnchor.color`. **value** will be a :ref:`type-color` or ``None``. It will have been normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ This is the environment implementation of :meth:`BaseAnchor.transformBy`. **matrix** will be a :ref:`type-transformation`. that has been normalized with :func:`normalizers.normalizeTransformationMatrix`. Subclasses may override this method. """ t = transform.Transform(*matrix) x, y = t.transformPoint((self.x, self.y)) self.x = x self.y = y # ------------- # Interpolation # ------------- compatibilityReporterClass = AnchorCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherAnchor) >>> compatible True >>> compatible [Warning] Anchor: "left" + "right" [Warning] Anchor: "left" has name left | "right" has name right This will return a ``bool`` indicating if the anchor is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseAnchor, self).isCompatible(other, BaseAnchor) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseAnchor.isCompatible`. Subclasses may override this method. """ anchor1 = self anchor2 = other # base names if anchor1.name != anchor2.name: reporter.nameDifference = True reporter.warning = True # ------------- # Normalization # ------------- def round(self): """ Round the anchor's coordinate. >>> anchor.round() This applies to the following: * x * y """ self._round() def _round(self): """ This is the environment implementation of :meth:`BaseAnchor.round`. Subclasses may override this method. """ self.x = normalizers.normalizeVisualRounding(self.x) self.y = normalizers.normalizeVisualRounding(self.y) robotools-fontParts-26e8b8c/Lib/fontParts/base/bPoint.py000066400000000000000000000274411477533125200233630ustar00rootroot00000000000000from fontTools.misc import transform from fontParts.base.base import ( BaseObject, TransformationMixin, SelectionMixin, IdentifierMixin, dynamicProperty, reference ) from fontParts.base.errors import FontPartsError from fontParts.base import normalizers from fontParts.base.deprecated import DeprecatedBPoint, RemovedBPoint class BaseBPoint( BaseObject, TransformationMixin, SelectionMixin, DeprecatedBPoint, IdentifierMixin, RemovedBPoint ): def _reprContents(self): contents = [ "%s" % self.type, "anchor='({x}, {y})'".format(x=self.anchor[0], y=self.anchor[1]), ] return contents def _setPoint(self, point): if hasattr(self, "_point"): raise AssertionError("point for bPoint already set") self._point = point def __eq__(self, other): if hasattr(other, "_point"): return self._point == other._point return NotImplemented # this class should not be used in hashable # collections since it is dynamically generated. __hash__ = None # ------- # Parents # ------- # identifier def _get_identifier(self): """ Subclasses may override this method. """ return self._point.identifier def _getIdentifier(self): """ Subclasses may override this method. """ return self._point.getIdentifier() # Segment _segment = dynamicProperty("base_segment") def _get_base_segment(self): point = self._point for segment in self.contour.segments: if segment.onCurve == point: return segment _nextSegment = dynamicProperty("base_nextSegment") def _get_base_nextSegment(self): contour = self.contour if contour is None: return None segments = contour.segments segment = self._segment i = segments.index(segment) + 1 if i >= len(segments): i = i % len(segments) nextSegment = segments[i] return nextSegment # Contour _contour = None contour = dynamicProperty("contour", "The bPoint's parent contour.") def _get_contour(self): if self._contour is None: return None return self._contour() def _set_contour(self, contour): if self._contour is not None: raise AssertionError("contour for bPoint already set") if contour is not None: contour = reference(contour) self._contour = contour # Glyph glyph = dynamicProperty("glyph", "The bPoint's parent glyph.") def _get_glyph(self): if self._contour is None: return None return self.contour.glyph # Layer layer = dynamicProperty("layer", "The bPoint's parent layer.") def _get_layer(self): if self._contour is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The bPoint's parent font.") def _get_font(self): if self._contour is None: return None return self.glyph.font # ---------- # Attributes # ---------- # anchor anchor = dynamicProperty("base_anchor", "The anchor point.") def _get_base_anchor(self): value = self._get_anchor() value = normalizers.normalizeCoordinateTuple(value) return value def _set_base_anchor(self, value): value = normalizers.normalizeCoordinateTuple(value) self._set_anchor(value) def _get_anchor(self): """ Subclasses may override this method. """ point = self._point return (point.x, point.y) def _set_anchor(self, value): """ Subclasses may override this method. """ pX, pY = self.anchor x, y = value dX = x - pX dY = y - pY self.moveBy((dX, dY)) # bcp in bcpIn = dynamicProperty("base_bcpIn", "The incoming off curve.") def _get_base_bcpIn(self): value = self._get_bcpIn() value = normalizers.normalizeCoordinateTuple(value) return value def _set_base_bcpIn(self, value): value = normalizers.normalizeCoordinateTuple(value) self._set_bcpIn(value) def _get_bcpIn(self): """ Subclasses may override this method. """ segment = self._segment offCurves = segment.offCurve if offCurves: bcp = offCurves[-1] x, y = relativeBCPIn(self.anchor, (bcp.x, bcp.y)) else: x = y = 0 return (x, y) def _set_bcpIn(self, value): """ Subclasses may override this method. """ x, y = absoluteBCPIn(self.anchor, value) segment = self._segment if segment.type == "move" and value != (0, 0): raise FontPartsError(("Cannot set the bcpIn for the first " "point in an open contour.") ) else: offCurves = segment.offCurve if offCurves: # if the two off curves are located at the anchor # coordinates we can switch to a line segment type. if value == (0, 0) and self.bcpOut == (0, 0): segment.type = "line" segment.smooth = False else: offCurves[-1].x = x offCurves[-1].y = y elif value != (0, 0): segment.type = "curve" offCurves = segment.offCurve offCurves[-1].x = x offCurves[-1].y = y # bcp out bcpOut = dynamicProperty("base_bcpOut", "The outgoing off curve.") def _get_base_bcpOut(self): value = self._get_bcpOut() value = normalizers.normalizeCoordinateTuple(value) return value def _set_base_bcpOut(self, value): value = normalizers.normalizeCoordinateTuple(value) self._set_bcpOut(value) def _get_bcpOut(self): """ Subclasses may override this method. """ nextSegment = self._nextSegment offCurves = nextSegment.offCurve if offCurves: bcp = offCurves[0] x, y = relativeBCPOut(self.anchor, (bcp.x, bcp.y)) else: x = y = 0 return (x, y) def _set_bcpOut(self, value): """ Subclasses may override this method. """ x, y = absoluteBCPOut(self.anchor, value) segment = self._segment nextSegment = self._nextSegment if nextSegment.type == "move" and value != (0, 0): raise FontPartsError(("Cannot set the bcpOut for the last " "point in an open contour.") ) else: offCurves = nextSegment.offCurve if offCurves: # if the off curves are located at the anchor coordinates # we can switch to a "line" segment type if value == (0, 0) and self.bcpIn == (0, 0): segment.type = "line" segment.smooth = False else: offCurves[0].x = x offCurves[0].y = y elif value != (0, 0): nextSegment.type = "curve" offCurves = nextSegment.offCurve offCurves[0].x = x offCurves[0].y = y # type type = dynamicProperty("base_type", "The bPoint type.") def _get_base_type(self): value = self._get_type() value = normalizers.normalizeBPointType(value) return value def _set_base_type(self, value): value = normalizers.normalizeBPointType(value) self._set_type(value) def _get_type(self): """ Subclasses may override this method. """ point = self._point typ = point.type bType = None if point.smooth: if typ == "curve": bType = "curve" elif typ == "line" or typ == "move": nextSegment = self._nextSegment if nextSegment is not None and nextSegment.type == "curve": bType = "curve" else: bType = "corner" elif typ in ("move", "line", "curve"): bType = "corner" if bType is None: raise FontPartsError("A %s point can not be converted to a bPoint." % typ) return bType def _set_type(self, value): """ Subclasses may override this method. """ point = self._point # convert corner to curve if value == "curve" and point.type == "line": # This needs to insert off curves without # generating unnecessary points in the # following segment. The segment object # implements this logic, so delegate the # change to the corresponding segment. segment = self._segment segment.type = "curve" segment.smooth = True # convert curve to corner elif value == "corner" and point.type == "curve": point.smooth = False # -------------- # Identification # -------------- index = dynamicProperty("index", ("The index of the bPoint within the ordered " "list of the parent contour's bPoints. None " "if the bPoint does not belong to a contour.") ) def _get_base_index(self): if self.contour is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Subclasses may override this method. """ contour = self.contour value = contour.bPoints.index(self) return value # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ anchor = self.anchor bcpIn = absoluteBCPIn(anchor, self.bcpIn) bcpOut = absoluteBCPOut(anchor, self.bcpOut) points = [bcpIn, anchor, bcpOut] t = transform.Transform(*matrix) bcpIn, anchor, bcpOut = t.transformPoints(points) x, y = anchor self._point.x = x self._point.y = y self.bcpIn = relativeBCPIn(anchor, bcpIn) self.bcpOut = relativeBCPOut(anchor, bcpOut) # ---- # Misc # ---- def round(self): """ Round coordinates. """ x, y = self.anchor self.anchor = (normalizers.normalizeVisualRounding(x), normalizers.normalizeVisualRounding(y)) x, y = self.bcpIn self.bcpIn = (normalizers.normalizeVisualRounding(x), normalizers.normalizeVisualRounding(y)) x, y = self.bcpOut self.bcpOut = (normalizers.normalizeVisualRounding(x), normalizers.normalizeVisualRounding(y)) def relativeBCPIn(anchor, BCPIn): """convert absolute incoming bcp value to a relative value""" return (BCPIn[0] - anchor[0], BCPIn[1] - anchor[1]) def absoluteBCPIn(anchor, BCPIn): """convert relative incoming bcp value to an absolute value""" return (BCPIn[0] + anchor[0], BCPIn[1] + anchor[1]) def relativeBCPOut(anchor, BCPOut): """convert absolute outgoing bcp value to a relative value""" return (BCPOut[0] - anchor[0], BCPOut[1] - anchor[1]) def absoluteBCPOut(anchor, BCPOut): """convert relative outgoing bcp value to an absolute value""" return (BCPOut[0] + anchor[0], BCPOut[1] + anchor[1]) robotools-fontParts-26e8b8c/Lib/fontParts/base/base.py000066400000000000000000000602651477533125200230430ustar00rootroot00000000000000import math from copy import deepcopy from fontTools.misc import transform from fontParts.base.errors import FontPartsError from fontParts.base import normalizers # ------- # Helpers # ------- class dynamicProperty(object): """ This implements functionality that is very similar to Python's built in property function, but makes it much easier for subclassing. Here is an example of why this is needed: class BaseObject(object): _foo = 1 def _get_foo(self): return self._foo def _set_foo(self, value): self._foo = value foo = property(_get_foo, _set_foo) class MyObject(BaseObject): def _set_foo(self, value): self._foo = value * 100 >>> m = MyObject() >>> m.foo 1 >>> m.foo = 2 >>> m.foo 2 The expected value is 200. The _set_foo method needs to be reregistered. Doing that also requires reregistering the _get_foo method. It's possible to do this, but it's messy and will make subclassing less than ideal. Using dynamicProperty solves this. class BaseObject(object): _foo = 1 foo = dynamicProperty("foo") def _get_foo(self): return self._foo def _set_foo(self, value): self._foo = value class MyObject(BaseObject): def _set_foo(self, value): self._foo = value * 100 >>> m = MyObject() >>> m.foo 1 >>> m.foo = 2 >>> m.foo 200 """ def __init__(self, name, doc=None): self.name = name self.__doc__ = doc self.getterName = "_get_" + name self.setterName = "_set_" + name def __get__(self, obj, cls): getter = getattr(obj, self.getterName, None) if getter is not None: return getter() else: # obj is None when the property is accessed # via the class instead of an instance if obj is None: return self raise FontPartsError("no getter for %r" % self.name) def __set__(self, obj, value): setter = getattr(obj, self.setterName, None) if setter is not None: setter(value) else: raise FontPartsError("no setter for %r" % self.name) def interpolate(a, b, v): return a + (b - a) * v # ------------ # Base Objects # ------------ class BaseObject(object): # -------------- # Initialization # -------------- def __init__(self, *args, **kwargs): self._init(*args, **kwargs) def _init(self, *args, **kwargs): """ Subclasses may override this method. """ pass # ---- # repr # ---- def __repr__(self): contents = self._reprContents() if contents: contents = " ".join(contents) contents = " " + contents else: contents = "" s = "<{className}{contents} at {address}>".format( className=self.__class__.__name__, contents=contents, address=id(self) ) return s @classmethod def _reprContents(cls): """ Subclasses may override this method to provide a list of strings for inclusion in ``__repr__``. If so, they should call ``super`` and append their additions to the returned ``list``. """ return [] # -------- # equality # -------- def __eq__(self, other): """ Subclasses may override this method. """ if isinstance(other, self.__class__): return self.naked() is other.naked() return NotImplemented def __ne__(self, other): """ Subclasses must not override this method. """ equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal # ---- # Hash # ---- def __hash__(self): """ Allow subclasses to be used in hashable collections. Subclasses may override this method. """ return id(self.naked()) # ---- # Copy # ---- copyClass = None copyAttributes = () def copy(self): """ Copy this object into a new object of the same type. The returned object will not have a parent object. """ copyClass = self.copyClass if copyClass is None: copyClass = self.__class__ copied = copyClass() copied.copyData(self) return copied def copyData(self, source): """ Subclasses may override this method. If so, they should call the super. """ for attr in self.copyAttributes: selfValue = getattr(self, attr) sourceValue = getattr(source, attr) if isinstance(selfValue, BaseObject): selfValue.copyData(sourceValue) else: setattr(self, attr, deepcopy(sourceValue)) # ---------- # Exceptions # ---------- def raiseNotImplementedError(self): """ This exception needs to be raised frequently by the base classes. So, it's here for convenience. """ raise NotImplementedError( "The {className} subclass does not implement this method." .format(className=self.__class__.__name__) ) # --------------------- # Environment Fallbacks # --------------------- def changed(self, *args, **kwargs): """ Tell the environment that something has changed in the object. The behavior of this method will vary from environment to environment. >>> obj.changed() """ def naked(self): """ Return the environment's native object that has been wrapped by this object. >>> loweLevelObj = obj.naked() """ self.raiseNotImplementedError() class BaseDict(BaseObject): keyNormalizer = None valueNormalizer = None def copyData(self, source): super(BaseDict, self).copyData(source) self.update(source) def __len__(self): value = self._len() return value def _len(self): """ Subclasses may override this method. """ return len(self.keys()) def keys(self): keys = self._keys() if self.keyNormalizer is not None: keys = [self.keyNormalizer.__func__(key) for key in keys] return keys def _keys(self): """ Subclasses may override this method. """ return [k for k, v in self.items()] def items(self): items = self._items() if self.keyNormalizer is not None and self.valueNormalizer is not None: values = [ (self.keyNormalizer.__func__(key), self.valueNormalizer.__func__(value)) for (key, value) in items ] return values def _items(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def values(self): values = self._values() if self.valueNormalizer is not None: values = [self.valueNormalizer.__func__(value) for value in values] return values def _values(self): """ Subclasses may override this method. """ return [v for k, v in self.items()] def __contains__(self, key): if self.keyNormalizer is not None: key = self.keyNormalizer.__func__(key) return self._contains(key) def _contains(self, key): """ Subclasses must override this method. """ self.raiseNotImplementedError() has_key = __contains__ def __setitem__(self, key, value): if self.keyNormalizer is not None: key = self.keyNormalizer.__func__(key) if self.valueNormalizer is not None: value = self.valueNormalizer.__func__(value) self._setItem(key, value) def _setItem(self, key, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() def __getitem__(self, key): if self.keyNormalizer is not None: key = self.keyNormalizer.__func__(key) value = self._getItem(key) if self.valueNormalizer is not None: value = self.valueNormalizer.__func__(value) return value def _getItem(self, key): """ Subclasses must override this method. """ self.raiseNotImplementedError() def get(self, key, default=None): if self.keyNormalizer is not None: key = self.keyNormalizer.__func__(key) if default is not None and self.valueNormalizer is not None: default = self.valueNormalizer.__func__(default) value = self._get(key, default=default) if value is not default and self.valueNormalizer is not None: value = self.valueNormalizer.__func__(value) return value def _get(self, key, default=None): """ Subclasses may override this method. """ if key in self: return self[key] return default def __delitem__(self, key): if self.keyNormalizer is not None: key = self.keyNormalizer.__func__(key) self._delItem(key) def _delItem(self, key): """ Subclasses must override this method. """ self.raiseNotImplementedError() def pop(self, key, default=None): if self.keyNormalizer is not None: key = self.keyNormalizer.__func__(key) if default is not None and self.valueNormalizer is not None: default = self.valueNormalizer.__func__(default) value = self._pop(key, default=default) if self.valueNormalizer is not None: value = self.valueNormalizer.__func__(value) return value def _pop(self, key, default=None): """ Subclasses may override this method. """ value = default if key in self: value = self[key] del self[key] return value def __iter__(self): return self._iter() def _iter(self): """ Subclasses may override this method. """ keys = self.keys() while keys: key = keys[0] yield key keys = keys[1:] def update(self, other): other = deepcopy(other) if self.keyNormalizer is not None and self.valueNormalizer is not None: d = {} for key, value in other.items(): key = self.keyNormalizer.__func__(key) value = self.valueNormalizer.__func__(value) d[key] = value value = d self._update(other) def _update(self, other): """ Subclasses may override this method. """ for key, value in other.items(): self[key] = value def clear(self): self._clear() def _clear(self): """ Subclasses may override this method. """ for key in self.keys(): del self[key] class TransformationMixin(object): # --------------- # Transformations # --------------- def transformBy(self, matrix, origin=None): """ Transform the object. >>> obj.transformBy((0.5, 0, 0, 2.0, 10, 0)) >>> obj.transformBy((0.5, 0, 0, 2.0, 10, 0), origin=(500, 500)) **matrix** must be a :ref:`type-transformation`. **origin** defines the point at with the transformation should originate. It must be a :ref:`type-coordinate` or ``None``. The default is ``(0, 0)``. """ matrix = normalizers.normalizeTransformationMatrix(matrix) if origin is None: origin = (0, 0) origin = normalizers.normalizeCoordinateTuple(origin) if origin is not None: t = transform.Transform() oX, oY = origin t = t.translate(oX, oY) t = t.transform(matrix) t = t.translate(-oX, -oY) matrix = tuple(t) self._transformBy(matrix) def _transformBy(self, matrix, **kwargs): """ This is the environment implementation of :meth:`BaseObject.transformBy`. **matrix** will be a :ref:`type-transformation`. that has been normalized with :func:`normalizers.normalizeTransformationMatrix`. Subclasses must override this method. """ self.raiseNotImplementedError() def moveBy(self, value): """ Move the object. >>> obj.moveBy((10, 0)) **value** must be an iterable containing two :ref:`type-int-float` values defining the x and y values to move the object by. """ value = normalizers.normalizeTransformationOffset(value) self._moveBy(value) def _moveBy(self, value, **kwargs): """ This is the environment implementation of :meth:`BaseObject.moveBy`. **value** will be an iterable containing two :ref:`type-int-float` values defining the x and y values to move the object by. It will have been normalized with :func:`normalizers.normalizeTransformationOffset`. Subclasses may override this method. """ x, y = value t = transform.Offset(x, y) self.transformBy(tuple(t), **kwargs) def scaleBy(self, value, origin=None): """ Scale the object. >>> obj.scaleBy(2.0) >>> obj.scaleBy((0.5, 2.0), origin=(500, 500)) **value** must be an iterable containing two :ref:`type-int-float` values defining the x and y values to scale the object by. **origin** defines the point at with the scale should originate. It must be a :ref:`type-coordinate` or ``None``. The default is ``(0, 0)``. """ value = normalizers.normalizeTransformationScale(value) if origin is None: origin = (0, 0) origin = normalizers.normalizeCoordinateTuple(origin) self._scaleBy(value, origin=origin) def _scaleBy(self, value, origin=None, **kwargs): """ This is the environment implementation of :meth:`BaseObject.scaleBy`. **value** will be an iterable containing two :ref:`type-int-float` values defining the x and y values to scale the object by. It will have been normalized with :func:`normalizers.normalizeTransformationScale`. **origin** will be a :ref:`type-coordinate` defining the point at which the scale should orginate. Subclasses may override this method. """ x, y = value t = transform.Identity.scale(x=x, y=y) self.transformBy(tuple(t), origin=origin, **kwargs) def rotateBy(self, value, origin=None): """ Rotate the object. >>> obj.rotateBy(45) >>> obj.rotateBy(45, origin=(500, 500)) **value** must be a :ref:`type-int-float` values defining the angle to rotate the object by. **origin** defines the point at with the rotation should originate. It must be a :ref:`type-coordinate` or ``None``. The default is ``(0, 0)``. """ value = normalizers.normalizeRotationAngle(value) if origin is None: origin = (0, 0) origin = normalizers.normalizeCoordinateTuple(origin) self._rotateBy(value, origin=origin) def _rotateBy(self, value, origin=None, **kwargs): """ This is the environment implementation of :meth:`BaseObject.rotateBy`. **value** will be a :ref:`type-int-float` value defining the value to rotate the object by. It will have been normalized with :func:`normalizers.normalizeRotationAngle`. **origin** will be a :ref:`type-coordinate` defining the point at which the rotation should orginate. Subclasses may override this method. """ a = math.radians(value) t = transform.Identity.rotate(a) self.transformBy(tuple(t), origin=origin, **kwargs) def skewBy(self, value, origin=None): """ Skew the object. >>> obj.skewBy(11) >>> obj.skewBy((25, 10), origin=(500, 500)) **value** must be rone of the following: * single :ref:`type-int-float` indicating the value to skew the x direction by. * iterable cointaining type :ref:`type-int-float` defining the values to skew the x and y directions by. **origin** defines the point at with the skew should originate. It must be a :ref:`type-coordinate` or ``None``. The default is ``(0, 0)``. """ value = normalizers.normalizeTransformationSkewAngle(value) if origin is None: origin = (0, 0) origin = normalizers.normalizeCoordinateTuple(origin) self._skewBy(value, origin=origin) def _skewBy(self, value, origin=None, **kwargs): """ This is the environment implementation of :meth:`BaseObject.skewBy`. **value** will be an iterable containing two :ref:`type-int-float` values defining the x and y values to skew the object by. It will have been normalized with :func:`normalizers.normalizeTransformationSkewAngle`. **origin** will be a :ref:`type-coordinate` defining the point at which the skew should orginate. Subclasses may override this method. """ x, y = value x = math.radians(x) y = math.radians(y) t = transform.Identity.skew(x=x, y=y) self.transformBy(tuple(t), origin=origin, **kwargs) class InterpolationMixin(object): # ------------- # Compatibility # ------------- compatibilityReporterClass = None def isCompatible(self, other, cls): """ Evaluate interpolation compatibility with other. """ if not isinstance(other, cls): raise TypeError( """Compatibility between an instance of %r and an \ instance of %r can not be checked.""" % (cls.__name__, other.__class__.__name__)) reporter = self.compatibilityReporterClass(self, other) self._isCompatible(other, reporter) return not reporter.fatal, reporter def _isCompatible(self, other, reporter): """ Subclasses must override this method. """ self.raiseNotImplementedError() class SelectionMixin(object): # ------------- # Selected Flag # ------------- selected = dynamicProperty( "base_selected", """ The object's selection state. >>> obj.selected False >>> obj.selected = True """ ) def _get_base_selected(self): value = self._get_selected() value = normalizers.normalizeBoolean(value) return value def _set_base_selected(self, value): value = normalizers.normalizeBoolean(value) self._set_selected(value) def _get_selected(self): """ This is the environment implementation of :attr:`BaseObject.selected`. This must return a **boolean** representing the selection state of the object. The value will be normalized with :func:`normalizers.normalizeBoolean`. Subclasses must override this method if they implement object selection. """ self.raiseNotImplementedError() def _set_selected(self, value): """ This is the environment implementation of :attr:`BaseObject.selected`. **value** will be a **boolean** representing the object's selection state. The value will have been normalized with :func:`normalizers.normalizeBoolean`. Subclasses must override this method if they implement object selection. """ self.raiseNotImplementedError() # ----------- # Sub-Objects # ----------- @classmethod def _getSelectedSubObjects(cls, subObjects): selected = [obj for obj in subObjects if obj.selected] return selected @classmethod def _setSelectedSubObjects(cls, subObjects, selected): for obj in subObjects: obj.selected = obj in selected class PointPositionMixin(object): """ This adds a ``position`` attribute as a dyanmicProperty, for use as a mixin with objects that have ``x`` and ``y`` attributes. """ position = dynamicProperty("base_position", "The point position.") def _get_base_position(self): value = self._get_position() value = normalizers.normalizeCoordinateTuple(value) return value def _set_base_position(self, value): value = normalizers.normalizeCoordinateTuple(value) self._set_position(value) def _get_position(self): """ Subclasses may override this method. """ return (self.x, self.y) def _set_position(self, value): """ Subclasses may override this method. """ pX, pY = self.position x, y = value dX = x - pX dY = y - pY self.moveBy((dX, dY)) class IdentifierMixin(object): # identifier identifier = dynamicProperty( "base_identifier", """ The unique identifier for the object. This value will be an :ref:`type-identifier` or a ``None``. This attribute is read only. :: >>> object.identifier 'ILHGJlygfds' To request an identifier if it does not exist use `object.getIdentifier()` """ ) def _get_base_identifier(self): value = self._get_identifier() if value is not None: value = normalizers.normalizeIdentifier(value) return value def _get_identifier(self): """ This is the environment implementation of :attr:`BaseObject.identifier`. This must return an :ref:`type-identifier`. If the native object does not have an identifier assigned one should be assigned and returned. Subclasses must override this method. """ self.raiseNotImplementedError() def getIdentifier(self): """ Create a new, unique identifier for and assign it to the object. If the object already has an identifier, the existing one should be returned. """ return self._getIdentifier() def _getIdentifier(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _setIdentifier(self, value): """ This method is used internally to force a specific identifier onto an object in certain situations. Subclasses that allow setting an identifier to a specific value may override this method. """ pass def reference(obj): """ This code returns a simple function that returns the given object. This is a backwards compatibility function that is under review. See #749. We used to use weak references, but they proved problematic (see issue #71), so this function was put in place to make sure existing code continued to function. The need for it is questionable, so it may be deleted soon. """ def wrapper(): return obj return wrapper class FuzzyNumber(object): """ A number like object with a threshold. Use it to compare numbers where a threshold is needed. """ def __init__(self, value, threshold): self.value = value self.threshold = threshold def __repr__(self): return "[%f %f]" % (self.value, self.threshold) def __lt__(self, other): if hasattr(other, "value"): if abs(self.value - other.value) < self.threshold: return False else: return self.value < other.value return self.value < other def __eq__(self, other): if hasattr(other, "value"): return abs(self.value - other.value) < self.threshold return self.value == other def __hash__(self): return hash((self.value, self.threshold)) robotools-fontParts-26e8b8c/Lib/fontParts/base/color.py000066400000000000000000000012431477533125200232360ustar00rootroot00000000000000class Color(tuple): """ An color object. This follows the :ref:`type-color`. """ def _get_r(self): return self[0] r = property(_get_r, "The color's red component as :ref:`type-int-float`.") def _get_g(self): return self[1] g = property(_get_g, "The color's green component as :ref:`type-int-float`.") def _get_b(self): return self[2] b = property(_get_b, "The color's blue component as :ref:`type-int-float`.") def _get_a(self): return self[3] a = property(_get_a, "The color's alpha component as :ref:`type-int-float`.") robotools-fontParts-26e8b8c/Lib/fontParts/base/compatibility.py000066400000000000000000000534021477533125200247750ustar00rootroot00000000000000from fontParts.base.base import dynamicProperty # ---- # Base # ---- class BaseCompatibilityReporter(object): objectName = "Base" def __init__(self, obj1, obj2): self._object1 = obj1 self._object2 = obj2 # status fatal = False warning = False def _get_title(self): title = "{object1Name} + {object2Name}".format( object1Name=self.object1Name, object2Name=self.object2Name ) if self.fatal: return self.formatFatalString(title) elif self.warning: return self.formatWarningString(title) else: return self.formatOKString(title) title = dynamicProperty("title") # objects object1 = dynamicProperty("object1") object1Name = dynamicProperty("object1Name") def _get_object1(self): return self._object1 def _get_object1Name(self): return self._getObjectName(self._object1) object2 = dynamicProperty("object2") object2Name = dynamicProperty("object2Name") def _get_object2(self): return self._object2 def _get_object2Name(self): return self._getObjectName(self._object2) @staticmethod def _getObjectName(obj): if hasattr(obj, "name") and obj.name is not None: return "\"%s\"" % obj.name elif hasattr(obj, "identifier") and obj.identifier is not None: return "\"%s\"" % obj.identifier elif hasattr(obj, "index"): return "[%s]" % obj.index else: return "<%s>" % id(obj) # Report def __repr__(self): return self.report() def report(self, showOK=False, showWarnings=False): raise NotImplementedError def formatFatalString(self, text): return "[Fatal] {objectName}: ".format(objectName=self.objectName) + text def formatWarningString(self, text): return "[Warning] {objectName}: ".format(objectName=self.objectName) + text def formatOKString(self, text): return "[OK] {objectName}: ".format(objectName=self.objectName) + text @staticmethod def reportSubObjects(reporters, showOK=True, showWarnings=True): report = [] for reporter in reporters: if showOK or reporter.fatal or (showWarnings and reporter.warning): report.append(repr(reporter)) return report @staticmethod def reportCountDifference(subObjectName, object1Name, object1Count, object2Name, object2Count): text = ("{object1Name} contains {object1Count} {subObjectName} | " "{object2Name} contains {object2Count} {subObjectName}").format( subObjectName=subObjectName, object1Name=object1Name, object1Count=object1Count, object2Name=object2Name, object2Count=object2Count ) return text @staticmethod def reportOrderDifference(subObjectName, object1Name, object1Order, object2Name, object2Order): text = ("{object1Name} has {subObjectName} ordered {object1Order} | " "{object2Name} has {object2Order}").format( subObjectName=subObjectName, object1Name=object1Name, object1Order=object1Order, object2Name=object2Name, object2Order=object2Order ) return text @staticmethod def reportDifferences(object1Name, subObjectName, subObjectID, object2Name): text = ("{object1Name} contains {subObjectName} {subObjectID} " "not in {object2Name}").format( object1Name=object1Name, subObjectName=subObjectName, subObjectID=subObjectID, object2Name=object2Name, ) return text # ---- # Font # ---- class FontCompatibilityReporter(BaseCompatibilityReporter): objectName = "Font" def __init__(self, font1, font2): super(FontCompatibilityReporter, self).__init__(font1, font2) self.guidelineCountDifference = False self.layerCountDifference = False self.guidelinesMissingFromFont2 = [] self.guidelinesMissingInFont1 = [] self.layersMissingFromFont2 = [] self.layersMissingInFont1 = [] self.layers = [] font1 = dynamicProperty("object1") font1Name = dynamicProperty("object1Name") font2 = dynamicProperty("object2") font2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): font1 = self.font1 font2 = self.font2 report = [] if self.guidelineCountDifference: text = self.reportCountDifference( subObjectName="guidelines", object1Name=self.font1Name, object1Count=len(font1.guidelines), object2Name=self.font2Name, object2Count=len(font2.guidelines) ) report.append(self.formatWarningString(text)) for name in self.guidelinesMissingFromFont2: text = self.reportDifferences( object1Name=self.font1Name, subObjectName="guideline", subObjectID=name, object2Name=self.font2Name, ) report.append(self.formatWarningString(text)) for name in self.guidelinesMissingInFont1: text = self.reportDifferences( object1Name=self.font2Name, subObjectName="guideline", subObjectID=name, object2Name=self.font1Name, ) report.append(self.formatWarningString(text)) if self.layerCountDifference: text = self.reportCountDifference( subObjectName="layers", object1Name=self.font1Name, object1Count=len(font1.layerOrder), object2Name=self.font2Name, object2Count=len(font2.layerOrder) ) report.append(self.formatWarningString(text)) for name in self.layersMissingFromFont2: text = self.reportDifferences( object1Name=self.font1Name, subObjectName="layer", subObjectID=name, object2Name=self.font2Name, ) report.append(self.formatWarningString(text)) for name in self.layersMissingInFont1: text = self.reportDifferences( object1Name=self.font2Name, subObjectName="layer", subObjectID=name, object2Name=self.font1Name, ) report.append(self.formatWarningString(text)) report += self.reportSubObjects(self.layers, showOK=showOK, showWarnings=showWarnings) if report or showOK: report.insert(0, self.title) return "\n".join(report) # ----- # Layer # ----- class LayerCompatibilityReporter(BaseCompatibilityReporter): objectName = "Layer" def __init__(self, layer1, layer2): super(LayerCompatibilityReporter, self).__init__(layer1, layer2) self.glyphCountDifference = False self.glyphsMissingFromLayer2 = [] self.glyphsMissingInLayer1 = [] self.glyphs = [] layer1 = dynamicProperty("object1") layer1Name = dynamicProperty("object1Name") layer2 = dynamicProperty("object2") layer2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): layer1 = self.layer1 layer2 = self.layer2 report = [] if self.glyphCountDifference: text = self.reportCountDifference( subObjectName="glyphs", object1Name=self.layer1Name, object1Count=len(layer1), object2Name=self.layer2Name, object2Count=len(layer2) ) report.append(self.formatWarningString(text)) for name in self.glyphsMissingFromLayer2: text = self.reportDifferences( object1Name=self.layer1Name, subObjectName="glyph", subObjectID=name, object2Name=self.layer2Name, ) report.append(self.formatWarningString(text)) for name in self.glyphsMissingInLayer1: text = self.reportDifferences( object1Name=self.layer2Name, subObjectName="glyph", subObjectID=name, object2Name=self.layer1Name, ) report.append(self.formatWarningString(text)) report += self.reportSubObjects(self.glyphs, showOK=showOK, showWarnings=showWarnings) if report or showOK: report.insert(0, self.title) return "\n".join(report) # ----- # Glyph # ----- class GlyphCompatibilityReporter(BaseCompatibilityReporter): objectName = "Glyph" def __init__(self, glyph1, glyph2): super(GlyphCompatibilityReporter, self).__init__(glyph1, glyph2) self.contourCountDifference = False self.componentCountDifference = False self.guidelineCountDifference = False self.anchorDifferences = [] self.anchorCountDifference = False self.anchorOrderDifference = False self.anchorsMissingFromGlyph1 = [] self.anchorsMissingFromGlyph2 = [] self.componentDifferences = [] self.componentOrderDifference = False self.componentsMissingFromGlyph1 = [] self.componentsMissingFromGlyph2 = [] self.guidelinesMissingFromGlyph1 = [] self.guidelinesMissingFromGlyph2 = [] self.contours = [] glyph1 = dynamicProperty("object1") glyph1Name = dynamicProperty("object1Name") glyph2 = dynamicProperty("object2") glyph2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): glyph1 = self.glyph1 glyph2 = self.glyph2 report = [] # Contour test if self.contourCountDifference: text = self.reportCountDifference( subObjectName="contours", object1Name=self.glyph1Name, object1Count=len(glyph1), object2Name=self.glyph2Name, object2Count=len(glyph2) ) report.append(self.formatFatalString(text)) report += self.reportSubObjects(self.contours, showOK=showOK, showWarnings=showWarnings) # Component test if self.componentCountDifference: text = self.reportCountDifference( subObjectName="components", object1Name=self.glyph1Name, object1Count=len(glyph1.components), object2Name=self.glyph2Name, object2Count=len(glyph2.components) ) report.append(self.formatFatalString(text)) elif self.componentOrderDifference: text = self.reportOrderDifference( subObjectName="components", object1Name=self.glyph1Name, object1Order=[c.baseGlyph for c in glyph1.components], object2Name=self.glyph2Name, object2Order=[c.baseGlyph for c in glyph2.components] ) report.append(self.formatWarningString(text)) for name in self.componentsMissingFromGlyph2: text = self.reportDifferences( object1Name=self.glyph1Name, subObjectName="component", subObjectID=name, object2Name=self.glyph2Name, ) report.append(self.formatWarningString(text)) for name in self.componentsMissingFromGlyph1: text = self.reportDifferences( object1Name=self.glyph2Name, subObjectName="component", subObjectID=name, object2Name=self.glyph1Name, ) report.append(self.formatWarningString(text)) # Anchor test if self.anchorCountDifference: text = self.reportCountDifference( subObjectName="anchors", object1Name=self.glyph1Name, object1Count=len(glyph1.anchors), object2Name=self.glyph2Name, object2Count=len(glyph2.anchors) ) report.append(self.formatWarningString(text)) elif self.anchorOrderDifference: text = self.reportOrderDifference( subObjectName="anchors", object1Name=self.glyph1Name, object1Order=[a.name for a in glyph1.anchors], object2Name=self.glyph2Name, object2Order=[a.name for a in glyph2.anchors] ) report.append(self.formatWarningString(text)) for name in self.anchorsMissingFromGlyph2: text = self.reportDifferences( object1Name=self.glyph1Name, subObjectName="anchor", subObjectID=name, object2Name=self.glyph2Name, ) report.append(self.formatWarningString(text)) for name in self.anchorsMissingFromGlyph1: text = self.reportDifferences( object1Name=self.glyph2Name, subObjectName="anchor", subObjectID=name, object2Name=self.glyph1Name, ) report.append(self.formatWarningString(text)) # Guideline test if self.guidelineCountDifference: text = self.reportCountDifference( subObjectName="guidelines", object1Name=self.glyph1Name, object1Count=len(glyph1.guidelines), object2Name=self.glyph2Name, object2Count=len(glyph2.guidelines) ) report.append(self.formatWarningString(text)) for name in self.guidelinesMissingFromGlyph2: text = self.reportDifferences( object1Name=self.glyph1Name, subObjectName="guideline", subObjectID=name, object2Name=self.glyph2Name, ) report.append(self.formatWarningString(text)) for name in self.guidelinesMissingFromGlyph1: text = self.reportDifferences( object1Name=self.glyph2Name, subObjectName="guideline", subObjectID=name, object2Name=self.glyph1Name, ) report.append(self.formatWarningString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report) # ------- # Contour # ------- class ContourCompatibilityReporter(BaseCompatibilityReporter): objectName = "Contour" def __init__(self, contour1, contour2): super(ContourCompatibilityReporter, self).__init__(contour1, contour2) self.openDifference = False self.directionDifference = False self.segmentCountDifference = False self.segments = [] contour1 = dynamicProperty("object1") contour1Name = dynamicProperty("object1Name") contour2 = dynamicProperty("object2") contour2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): contour1 = self.contour1 contour2 = self.contour2 report = [] if self.segmentCountDifference: text = self.reportCountDifference( subObjectName="segments", object1Name=self.contour1Name, object1Count=len(contour1), object2Name=self.contour2Name, object2Count=len(contour2) ) report.append(self.formatFatalString(text)) if self.openDifference: state1 = state2 = "closed" if contour1.open: state1 = "open" if contour2.open: state2 = "open" text = "{contour1Name} is {state1} | {contour2Name} is {state2}".format( contour1Name=self.contour1Name, state1=state1, contour2Name=self.contour2Name, state2=state2 ) report.append(self.formatFatalString(text)) if self.directionDifference: state1 = state2 = "counter-clockwise" if contour1.clockwise: state1 = "clockwise" if contour2.clockwise: state2 = "clockwise" text = "{contour1Name} is {state1} | {contour2Name} is {state2}".format( contour1Name=self.contour1Name, state1=state1, contour2Name=self.contour2Name, state2=state2 ) report.append(self.formatFatalString(text)) report += self.reportSubObjects(self.segments, showOK=showOK, showWarnings=showWarnings) if report or showOK: report.insert(0, self.title) return "\n".join(report) # ------- # Segment # ------- class SegmentCompatibilityReporter(BaseCompatibilityReporter): objectName = "Segment" def __init__(self, contour1, contour2): super(SegmentCompatibilityReporter, self).__init__(contour1, contour2) self.typeDifference = False segment1 = dynamicProperty("object1") segment1Name = dynamicProperty("object1Name") segment2 = dynamicProperty("object2") segment2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): segment1 = self.segment1 segment2 = self.segment2 report = [] if self.typeDifference: type1 = segment1.type type2 = segment2.type text = "{segment1Name} is {type1} | {segment2Name} is {type2}".format( segment1Name=self.segment1Name, type1=type1, segment2Name=self.segment2Name, type2=type2 ) report.append(self.formatFatalString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report) # --------- # Component # --------- class ComponentCompatibilityReporter(BaseCompatibilityReporter): objectName = "Component" def __init__(self, component1, component2): super(ComponentCompatibilityReporter, self).__init__(component1, component2) self.baseDifference = False component1 = dynamicProperty("object1") component1Name = dynamicProperty("object1Name") component2 = dynamicProperty("object2") component2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): component1 = self.component1 component2 = self.component2 report = [] if self.baseDifference: name1 = component1.baseName name2 = component2.baseName text = ("{component1Name} has base glyph {name1} | " "{component2Name} has base glyph {name2}").format( component1Name=self.component1Name, name1=name1, component2Name=self.component2Name, name2=name2 ) report.append(self.formatWarningString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report) # ------ # Anchor # ------ class AnchorCompatibilityReporter(BaseCompatibilityReporter): objectName = "Anchor" def __init__(self, anchor1, anchor2): super(AnchorCompatibilityReporter, self).__init__(anchor1, anchor2) self.nameDifference = False anchor1 = dynamicProperty("object1") anchor1Name = dynamicProperty("object1Name") anchor2 = dynamicProperty("object2") anchor2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): anchor1 = self.anchor1 anchor2 = self.anchor2 report = [] if self.nameDifference: name1 = anchor1.name name2 = anchor2.name text = ("{anchor1Name} has name {name1} | " "{anchor2Name} has name {name2}").format( anchor1Name=self.anchor1Name, name1=name1, anchor2Name=self.anchor2Name, name2=name2 ) report.append(self.formatWarningString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report) # --------- # Guideline # --------- class GuidelineCompatibilityReporter(BaseCompatibilityReporter): objectName = "Guideline" def __init__(self, guideline1, guideline2): super(GuidelineCompatibilityReporter, self).__init__(guideline1, guideline2) self.nameDifference = False guideline1 = dynamicProperty("object1") guideline1Name = dynamicProperty("object1Name") guideline2 = dynamicProperty("object2") guideline2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): guideline1 = self.guideline1 guideline2 = self.guideline2 report = [] if self.nameDifference: name1 = guideline1.name name2 = guideline2.name text = ("{guideline1Name} has name {name1} | " "{guideline2Name} has name {name2}").format( guideline1Name=self.guideline1Name, name1=name1, guideline2Name=self.guideline2Name, name2=name2 ) report.append(self.formatWarningString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report) robotools-fontParts-26e8b8c/Lib/fontParts/base/component.py000066400000000000000000000257061477533125200241340ustar00rootroot00000000000000from fontTools.misc import transform from fontParts.base import normalizers from fontParts.base.errors import FontPartsError from fontParts.base.base import ( BaseObject, TransformationMixin, InterpolationMixin, PointPositionMixin, SelectionMixin, IdentifierMixin, dynamicProperty, reference ) from fontParts.base.compatibility import ComponentCompatibilityReporter from fontParts.base.deprecated import DeprecatedComponent, RemovedComponent class BaseComponent( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, IdentifierMixin, DeprecatedComponent, RemovedComponent ): copyAttributes = ( "baseGlyph", "transformation" ) def _reprContents(self): contents = [ "baseGlyph='%s'" % self.baseGlyph, "offset='({x}, {y})'".format(x=self.offset[0], y=self.offset[1]), ] if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() return contents # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The component's parent glyph.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._glyph is not None: raise AssertionError("glyph for component already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Layer layer = dynamicProperty("layer", "The component's parent layer.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The component's parent font.") def _get_font(self): if self._glyph is None: return None return self.glyph.font # ---------- # Attributes # ---------- # baseGlyph baseGlyph = dynamicProperty("base_baseGlyph", "The name of the glyph the component references.") def _get_base_baseGlyph(self): value = self._get_baseGlyph() # if the component does not belong to a layer, # it is allowed to have None as its baseGlyph if value is None and self.layer is None: pass else: value = normalizers.normalizeGlyphName(value) return value def _set_base_baseGlyph(self, value): value = normalizers.normalizeGlyphName(value) self._set_baseGlyph(value) def _get_baseGlyph(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_baseGlyph(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # transformation transformation = dynamicProperty("base_transformation", "The component's transformation matrix.") def _get_base_transformation(self): value = self._get_transformation() value = normalizers.normalizeTransformationMatrix(value) return value def _set_base_transformation(self, value): value = normalizers.normalizeTransformationMatrix(value) self._set_transformation(value) def _get_transformation(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_transformation(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # offset offset = dynamicProperty("base_offset", "The component's offset.") def _get_base_offset(self): value = self._get_offset() value = normalizers.normalizeTransformationOffset(value) return value def _set_base_offset(self, value): value = normalizers.normalizeTransformationOffset(value) self._set_offset(value) def _get_offset(self): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation return ox, oy def _set_offset(self, value): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation ox, oy = value self.transformation = sx, sxy, syx, sy, ox, oy # scale scale = dynamicProperty("base_scale", "The component's scale.") def _get_base_scale(self): value = self._get_scale() value = normalizers.normalizeComponentScale(value) return value def _set_base_scale(self, value): value = normalizers.normalizeComponentScale(value) self._set_scale(value) def _get_scale(self): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation return sx, sy def _set_scale(self, value): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation sx, sy = value self.transformation = sx, sxy, syx, sy, ox, oy # -------------- # Identification # -------------- # index index = dynamicProperty("base_index", ("The index of the component within the " "ordered list of the parent glyph's components.")) def _get_base_index(self): glyph = self.glyph if glyph is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _set_base_index(self, value): glyph = self.glyph if glyph is None: raise FontPartsError("The component does not belong to a glyph.") value = normalizers.normalizeIndex(value) componentCount = len(glyph.components) if value < 0: value = -(value % componentCount) if value >= componentCount: value = componentCount self._set_index(value) def _get_index(self): """ Subclasses may override this method. """ glyph = self.glyph return glyph.components.index(self) def _set_index(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # ---- # Pens # ---- def draw(self, pen): """ Draw the component with the given Pen. """ self._draw(pen) def _draw(self, pen, **kwargs): """ Subclasses may override this method. """ from fontTools.ufoLib.pointPen import PointToSegmentPen adapter = PointToSegmentPen(pen) self.drawPoints(adapter) def drawPoints(self, pen): """ Draw the contour with the given PointPen. """ self._drawPoints(pen) def _drawPoints(self, pen, **kwargs): """ Subclasses may override this method. """ # The try: ... except TypeError: ... # handles backwards compatibility with # point pens that have not been upgraded # to point pen protocol 2. try: pen.addComponent(self.baseGlyph, self.transformation, identifier=self.identifier, **kwargs) except TypeError: pen.addComponent(self.baseGlyph, self.transformation, **kwargs) # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ t = transform.Transform(*matrix) transformation = t.transform(self.transformation) self.transformation = tuple(transformation) # ------------- # Normalization # ------------- def round(self): """ Round offset coordinates. """ self._round() def _round(self): """ Subclasses may override this method. """ x, y = self.offset x = normalizers.normalizeVisualRounding(x) y = normalizers.normalizeVisualRounding(y) self.offset = (x, y) def decompose(self): """ Decompose the component. """ glyph = self.glyph if glyph is None: raise FontPartsError("The component does not belong to a glyph.") self._decompose() def _decompose(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() # ------------- # Interpolation # ------------- compatibilityReporterClass = ComponentCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherComponent) >>> compatible True >>> compatible [Warning] Component: "A" + "B" [Warning] Component: "A" has name A | "B" has name B This will return a ``bool`` indicating if the component is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseComponent, self).isCompatible(other, BaseComponent) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseComponent.isCompatible`. Subclasses may override this method. """ component1 = self component2 = other # base glyphs if component1.baseName != component2.baseName: reporter.baseDifference = True reporter.warning = True # ------------ # Data Queries # ------------ def pointInside(self, point): """ Determine if point is in the black or white of the component. point must be an (x, y) tuple. """ point = normalizers.normalizeCoordinateTuple(point) return self._pointInside(point) def _pointInside(self, point): """ Subclasses may override this method. """ from fontTools.pens.pointInsidePen import PointInsidePen pen = PointInsidePen(glyphSet=self.layer, testPoint=point, evenOdd=False) self.draw(pen) return pen.getResult() bounds = dynamicProperty("base_bounds", ("The bounds of the component: " "(xMin, yMin, xMax, yMax) or None.")) def _get_base_bounds(self): value = self._get_bounds() if value is not None: value = normalizers.normalizeBoundingBox(value) return value def _get_bounds(self): """ Subclasses may override this method. """ from fontTools.pens.boundsPen import BoundsPen pen = BoundsPen(self.layer) self.draw(pen) return pen.bounds robotools-fontParts-26e8b8c/Lib/fontParts/base/contour.py000066400000000000000000001026421477533125200236160ustar00rootroot00000000000000from fontParts.base.errors import FontPartsError from fontParts.base.base import ( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, IdentifierMixin, dynamicProperty, reference ) from fontParts.base import normalizers from fontParts.base.compatibility import ContourCompatibilityReporter from fontParts.base.bPoint import absoluteBCPIn, absoluteBCPOut from fontParts.base.deprecated import DeprecatedContour, RemovedContour class BaseContour( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, IdentifierMixin, DeprecatedContour, RemovedContour ): segmentClass = None bPointClass = None def _reprContents(self): contents = [] if self.identifier is not None: contents.append("identifier='%r'" % self.identifier) if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() return contents def copyData(self, source): super(BaseContour, self).copyData(source) for sourcePoint in source.points: self.appendPoint((0, 0)) selfPoint = self.points[-1] selfPoint.copyData(sourcePoint) # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The contour's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._glyph is not None: raise AssertionError("glyph for contour already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Font font = dynamicProperty("font", "The contour's parent font.") def _get_font(self): if self._glyph is None: return None return self.glyph.font # Layer layer = dynamicProperty("layer", "The contour's parent layer.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # -------------- # Identification # -------------- # index index = dynamicProperty( "base_index", """ The index of the contour within the parent glyph's contours. >>> contour.index 1 >>> contour.index = 0 The value will always be a :ref:`type-int`. """ ) def _get_base_index(self): glyph = self.glyph if glyph is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _set_base_index(self, value): glyph = self.glyph if glyph is None: raise FontPartsError("The contour does not belong to a glyph.") value = normalizers.normalizeIndex(value) contourCount = len(glyph.contours) if value < 0: value = -(value % contourCount) if value >= contourCount: value = contourCount self._set_index(value) def _get_index(self): """ Subclasses may override this method. """ glyph = self.glyph return glyph.contours.index(self) def _set_index(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # identifier def getIdentifierForPoint(self, point): """ Create a unique identifier for and assign it to ``point``. If the point already has an identifier, the existing identifier will be returned. >>> contour.getIdentifierForPoint(point) 'ILHGJlygfds' ``point`` must be a :class:`BasePoint`. The returned value will be a :ref:`type-identifier`. """ point = normalizers.normalizePoint(point) return self._getIdentifierforPoint(point) def _getIdentifierforPoint(self, point): """ Subclasses must override this method. """ self.raiseNotImplementedError() # ---- # Pens # ---- def draw(self, pen): """ Draw the contour's outline data to the given :ref:`type-pen`. >>> contour.draw(pen) """ self._draw(pen) def _draw(self, pen, **kwargs): """ Subclasses may override this method. """ from fontTools.ufoLib.pointPen import PointToSegmentPen adapter = PointToSegmentPen(pen) self.drawPoints(adapter) def drawPoints(self, pen): """ Draw the contour's outline data to the given :ref:`type-point-pen`. >>> contour.drawPoints(pointPen) """ self._drawPoints(pen) def _drawPoints(self, pen, **kwargs): """ Subclasses may override this method. """ # The try: ... except TypeError: ... # handles backwards compatibility with # point pens that have not been upgraded # to point pen protocol 2. try: pen.beginPath(self.identifier) except TypeError: pen.beginPath() for point in self.points: typ = point.type if typ == "offcurve": typ = None try: pen.addPoint(pt=(point.x, point.y), segmentType=typ, smooth=point.smooth, name=point.name, identifier=point.identifier) except TypeError: pen.addPoint(pt=(point.x, point.y), segmentType=typ, smooth=point.smooth, name=point.name) pen.endPath() # ------------------ # Data normalization # ------------------ def autoStartSegment(self): """ Automatically calculate and set the first segment in this contour. The behavior of this may vary accross environments. """ self._autoStartSegment() def _autoStartSegment(self, **kwargs): """ Subclasses may override this method. XXX port this from robofab """ self.raiseNotImplementedError() def round(self): """ Round coordinates in all points to integers. """ self._round() def _round(self, **kwargs): """ Subclasses may override this method. """ for point in self.points: point.round() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ for point in self.points: point.transformBy(matrix) # ------------- # Interpolation # ------------- compatibilityReporterClass = ContourCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherContour) >>> compatible False >>> compatible [Fatal] Contour: [0] + [0] [Fatal] Contour: [0] contains 4 segments | [0] contains 3 segments [Fatal] Contour: [0] is closed | [0] is open This will return a ``bool`` indicating if the contour is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseContour, self).isCompatible(other, BaseContour) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseContour.isCompatible`. Subclasses may override this method. """ contour1 = self contour2 = other # open/closed if contour1.open != contour2.open: reporter.openDifference = True # direction if contour1.clockwise != contour2.clockwise: reporter.directionDifference = True # segment count if len(contour1) != len(contour2.segments): reporter.segmentCountDifference = True reporter.fatal = True # segment pairs for i in range(min(len(contour1), len(contour2))): segment1 = contour1[i] segment2 = contour2[i] segmentCompatibility = segment1.isCompatible(segment2)[1] if segmentCompatibility.fatal or segmentCompatibility.warning: if segmentCompatibility.fatal: reporter.fatal = True if segmentCompatibility.warning: reporter.warning = True reporter.segments.append(segmentCompatibility) # ---- # Open # ---- open = dynamicProperty("base_open", "Boolean indicating if the contour is open.") def _get_base_open(self): value = self._get_open() value = normalizers.normalizeBoolean(value) return value def _get_open(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() # --------- # Direction # --------- clockwise = dynamicProperty("base_clockwise", ("Boolean indicating if the contour's " "winding direction is clockwise.")) def _get_base_clockwise(self): value = self._get_clockwise() value = normalizers.normalizeBoolean(value) return value def _set_base_clockwise(self, value): value = normalizers.normalizeBoolean(value) self._set_clockwise(value) def _get_clockwise(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_clockwise(self, value): """ Subclasses may override this method. """ if self.clockwise != value: self.reverse() def reverse(self): """ Reverse the direction of the contour. """ self._reverseContour() def _reverse(self, **kwargs): """ Subclasses may override this method. """ self.raiseNotImplementedError() # ------------------------ # Point and Contour Inside # ------------------------ def pointInside(self, point): """ Determine if ``point`` is in the black or white of the contour. >>> contour.pointInside((40, 65)) True ``point`` must be a :ref:`type-coordinate`. """ point = normalizers.normalizeCoordinateTuple(point) return self._pointInside(point) def _pointInside(self, point): """ Subclasses may override this method. """ from fontTools.pens.pointInsidePen import PointInsidePen pen = PointInsidePen(glyphSet=None, testPoint=point, evenOdd=False) self.draw(pen) return pen.getResult() def contourInside(self, otherContour): """ Determine if ``otherContour`` is in the black or white of this contour. >>> contour.contourInside(otherContour) True ``contour`` must be a :class:`BaseContour`. """ otherContour = normalizers.normalizeContour(otherContour) return self._contourInside(otherContour) def _contourInside(self, otherContour): """ Subclasses may override this method. """ self.raiseNotImplementedError() # --------------- # Bounds and Area # --------------- bounds = dynamicProperty("bounds", ("The bounds of the contour: " "(xMin, yMin, xMax, yMax) or None.")) def _get_base_bounds(self): value = self._get_bounds() if value is not None: value = normalizers.normalizeBoundingBox(value) return value def _get_bounds(self): """ Subclasses may override this method. """ from fontTools.pens.boundsPen import BoundsPen pen = BoundsPen(self.layer) self.draw(pen) return pen.bounds area = dynamicProperty("area", ("The area of the contour: " "A positive number or None.")) def _get_base_area(self): value = self._get_area() if value is not None: value = normalizers.normalizeArea(value) return value def _get_area(self): """ Subclasses may override this method. """ from fontTools.pens.areaPen import AreaPen pen = AreaPen(self.layer) self.draw(pen) return abs(pen.value) # -------- # Segments # -------- # The base class implements the full segment interaction API. # Subclasses do not need to override anything within the contour # other than registering segmentClass. Subclasses may choose to # implement this API independently if desired. def _setContourInSegment(self, segment): if segment.contour is None: segment.contour = self segments = dynamicProperty("segments") def _get_segments(self): """ Subclasses may override this method. """ points = list(self.points) if not points: return [] segments = [[]] lastWasOffCurve = False firstIsMove = points[0].type == "move" for point in points: segments[-1].append(point) if point.type != "offcurve": segments.append([]) lastWasOffCurve = point.type == "offcurve" if len(segments[-1]) == 0: del segments[-1] if lastWasOffCurve and firstIsMove: # ignore trailing off curves del segments[-1] if lastWasOffCurve and not firstIsMove and len(segments) > 1: segment = segments.pop(-1) segment.extend(segments[0]) del segments[0] segments.append(segment) if not lastWasOffCurve and not firstIsMove: segment = segments.pop(0) segments.append(segment) # wrap into segments wrapped = [] for points in segments: s = self.segmentClass() s._setPoints(points) self._setContourInSegment(s) wrapped.append(s) return wrapped def __getitem__(self, index): return self.segments[index] def __iter__(self): return self._iterSegments() def _iterSegments(self): segments = self.segments count = len(segments) index = 0 while count: yield segments[index] count -= 1 index += 1 def __len__(self): return self._len__segments() def _len__segments(self, **kwargs): """ Subclasses may override this method. """ return len(self.segments) def appendSegment(self, type=None, points=None, smooth=False, segment=None): """ Append a segment to the contour. """ if segment is not None: if type is not None: type = segment.type if points is None: points = [(point.x, point.y) for point in segment.points] smooth = segment.smooth type = normalizers.normalizeSegmentType(type) pts = [] for pt in points: pt = normalizers.normalizeCoordinateTuple(pt) pts.append(pt) points = pts smooth = normalizers.normalizeBoolean(smooth) self._appendSegment(type=type, points=points, smooth=smooth) def _appendSegment(self, type=None, points=None, smooth=False, **kwargs): """ Subclasses may override this method. """ self._insertSegment(len(self), type=type, points=points, smooth=smooth, **kwargs) def insertSegment(self, index, type=None, points=None, smooth=False, segment=None): """ Insert a segment into the contour. """ if segment is not None: if type is not None: type = segment.type if points is None: points = [(point.x, point.y) for point in segment.points] smooth = segment.smooth index = normalizers.normalizeIndex(index) type = normalizers.normalizeSegmentType(type) pts = [] for pt in points: pt = normalizers.normalizeCoordinateTuple(pt) pts.append(pt) points = pts smooth = normalizers.normalizeBoolean(smooth) self._insertSegment(index=index, type=type, points=points, smooth=smooth) def _insertSegment(self, index=None, type=None, points=None, smooth=False, **kwargs): """ Subclasses may override this method. """ onCurve = points[-1] offCurve = points[:-1] segments = self.segments addPointCount = 1 if self.open: index += 1 addPointCount = 0 ptCount = sum([len(segments[s].points) for s in range(index)]) + addPointCount self.insertPoint(ptCount, onCurve, type=type, smooth=smooth) for offCurvePoint in reversed(offCurve): self.insertPoint(ptCount, offCurvePoint, type="offcurve") def removeSegment(self, segment, preserveCurve=False): """ Remove segment from the contour. If ``preserveCurve`` is set to ``True`` an attempt will be made to preserve the shape of the curve if the environment supports that functionality. """ if not isinstance(segment, int): segment = self.segments.index(segment) segment = normalizers.normalizeIndex(segment) if segment >= self._len__segments(): raise ValueError("No segment located at index %d." % segment) preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removeSegment(segment, preserveCurve) def _removeSegment(self, segment, preserveCurve, **kwargs): """ segment will be a valid segment index. preserveCurve will be a boolean. Subclasses may override this method. """ segment = self.segments[segment] for point in segment.points: self.removePoint(point, preserveCurve) def setStartSegment(self, segment): """ Set the first segment on the contour. segment can be a segment object or an index. """ if self.open: raise FontPartsError("An open contour can not change the starting segment.") segments = self.segments if not isinstance(segment, int): segmentIndex = segments.index(segment) else: segmentIndex = segment if len(self.segments) < 2: return if segmentIndex == 0: return if segmentIndex >= len(segments): raise ValueError(("The contour does not contain a segment at index %d" % segmentIndex)) self._setStartSegment(segmentIndex) def _setStartSegment(self, segmentIndex, **kwargs): """ Subclasses may override this method. """ # get the previous segment and set # its on curve as the first point # in the contour. this matches the # iteration behavior of self.segments. segmentIndex -= 1 segments = self.segments segment = segments[segmentIndex] self.setStartPoint(segment.points[-1]) # ------- # bPoints # ------- bPoints = dynamicProperty("bPoints") def _get_bPoints(self): bPoints = [] for point in self.points: if point.type not in ("move", "line", "curve"): continue bPoint = self.bPointClass() bPoint.contour = self bPoint._setPoint(point) bPoints.append(bPoint) return tuple(bPoints) def appendBPoint(self, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None): """ Append a bPoint to the contour. """ if bPoint is not None: if type is None: type = bPoint.type if anchor is None: anchor = bPoint.anchor if bcpIn is None: bcpIn = bPoint.bcpIn if bcpOut is None: bcpOut = bPoint.bcpOut type = normalizers.normalizeBPointType(type) anchor = normalizers.normalizeCoordinateTuple(anchor) if bcpIn is None: bcpIn = (0, 0) bcpIn = normalizers.normalizeCoordinateTuple(bcpIn) if bcpOut is None: bcpOut = (0, 0) bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._appendBPoint(type, anchor, bcpIn=bcpIn, bcpOut=bcpOut) def _appendBPoint(self, type, anchor, bcpIn=None, bcpOut=None, **kwargs): """ Subclasses may override this method. """ self.insertBPoint( len(self.bPoints), type, anchor, bcpIn=bcpIn, bcpOut=bcpOut ) def insertBPoint(self, index, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None): """ Insert a bPoint at index in the contour. """ if bPoint is not None: if type is None: type = bPoint.type if anchor is None: anchor = bPoint.anchor if bcpIn is None: bcpIn = bPoint.bcpIn if bcpOut is None: bcpOut = bPoint.bcpOut index = normalizers.normalizeIndex(index) type = normalizers.normalizeBPointType(type) anchor = normalizers.normalizeCoordinateTuple(anchor) if bcpIn is None: bcpIn = (0, 0) bcpIn = normalizers.normalizeCoordinateTuple(bcpIn) if bcpOut is None: bcpOut = (0, 0) bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._insertBPoint(index=index, type=type, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut) def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs): """ Subclasses may override this method. """ # insert a simple line segment at the given anchor # look it up as a bPoint and change the bcpIn and bcpOut there # this avoids code duplication self._insertSegment(index=index, type="line", points=[anchor], smooth=False) bPoints = self.bPoints index += 1 if index >= len(bPoints): # its an append instead of an insert # so take the last bPoint index = -1 bPoint = bPoints[index] bPoint.bcpIn = bcpIn bPoint.bcpOut = bcpOut bPoint.type = type def removeBPoint(self, bPoint): """ Remove the bpoint from the contour. bpoint can be a point object or an index. """ if not isinstance(bPoint, int): bPoint = bPoint.index bPoint = normalizers.normalizeIndex(bPoint) if bPoint >= self._len__points(): raise ValueError("No bPoint located at index %d." % bPoint) self._removeBPoint(bPoint) def _removeBPoint(self, index, **kwargs): """ index will be a valid index. Subclasses may override this method. """ bPoint = self.bPoints[index] nextSegment = bPoint._nextSegment offCurves = nextSegment.offCurve if offCurves: offCurve = offCurves[0] self.removePoint(offCurve) segment = bPoint._segment offCurves = segment.offCurve if offCurves: offCurve = offCurves[-1] self.removePoint(offCurve) self.removePoint(bPoint._point) # ------ # Points # ------ def _setContourInPoint(self, point): if point.contour is None: point.contour = self points = dynamicProperty("points") def _get_points(self): """ Subclasses may override this method. """ return tuple([self._getitem__points(i) for i in range(self._len__points())]) def _len__points(self): return self._lenPoints() def _lenPoints(self, **kwargs): """ This must return an integer indicating the number of points in the contour. Subclasses must override this method. """ self.raiseNotImplementedError() def _getitem__points(self, index): index = normalizers.normalizeIndex(index) if index >= self._len__points(): raise ValueError("No point located at index %d." % index) point = self._getPoint(index) self._setContourInPoint(point) return point def _getPoint(self, index, **kwargs): """ This must return a wrapped point. index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def _getPointIndex(self, point): for i, other in enumerate(self.points): if point == other: return i raise FontPartsError("The point could not be found.") def appendPoint(self, position=None, type="line", smooth=False, name=None, identifier=None, point=None): """ Append a point to the contour. """ if point is not None: if position is None: position = point.position type = point.type smooth = point.smooth if name is None: name = point.name if identifier is not None: identifier = point.identifier self.insertPoint( len(self.points), position=position, type=type, smooth=smooth, name=name, identifier=identifier ) def insertPoint(self, index, position=None, type="line", smooth=False, name=None, identifier=None, point=None): """ Insert a point into the contour. """ if point is not None: if position is None: position = point.position type = point.type smooth = point.smooth if name is None: name = point.name if identifier is not None: identifier = point.identifier index = normalizers.normalizeIndex(index) position = normalizers.normalizeCoordinateTuple(position) type = normalizers.normalizePointType(type) smooth = normalizers.normalizeBoolean(smooth) if name is not None: name = normalizers.normalizePointName(name) if identifier is not None: identifier = normalizers.normalizeIdentifier(identifier) self._insertPoint( index, position=position, type=type, smooth=smooth, name=name, identifier=identifier ) def _insertPoint(self, index, position, type="line", smooth=False, name=None, identifier=None, **kwargs): """ position will be a valid position (x, y). type will be a valid type. smooth will be a valid boolean. name will be a valid name or None. identifier will be a valid identifier or None. The identifier will not have been tested for uniqueness. Subclasses must override this method. """ self.raiseNotImplementedError() def removePoint(self, point, preserveCurve=False): """ Remove the point from the contour. point can be a point object or an index. If ``preserveCurve`` is set to ``True`` an attempt will be made to preserve the shape of the curve if the environment supports that functionality. """ if not isinstance(point, int): point = self.points.index(point) point = normalizers.normalizeIndex(point) if point >= self._len__points(): raise ValueError("No point located at index %d." % point) preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removePoint(point, preserveCurve) def _removePoint(self, index, preserveCurve, **kwargs): """ index will be a valid index. preserveCurve will be a boolean. Subclasses must override this method. """ self.raiseNotImplementedError() def setStartPoint(self, point): """ Set the first point on the contour. point can be a point object or an index. """ if self.open: raise FontPartsError("An open contour can not change the starting point.") points = self.points if not isinstance(point, int): pointIndex = points.index(point) else: pointIndex = point if pointIndex == 0: return if pointIndex >= len(points): raise ValueError(("The contour does not contain a point at index %d" % pointIndex)) self._setStartPoint(pointIndex) def _setStartPoint(self, pointIndex, **kwargs): """ Subclasses may override this method. """ points = self.points points = points[pointIndex:] + points[:pointIndex] # Clear the points. for point in self.points: self.removePoint(point) # Add the points. for point in points: self.appendPoint( (point.x, point.y), type=point.type, smooth=point.smooth, name=point.name, identifier=point.identifier ) # --------- # Selection # --------- # segments selectedSegments = dynamicProperty( "base_selectedSegments", """ A list of segments selected in the contour. Getting selected segment objects: >>> for segment in contour.selectedSegments: ... segment.move((10, 20)) Setting selected segment objects: >>> contour.selectedSegments = someSegments Setting also supports segment indexes: >>> contour.selectedSegments = [0, 2] """ ) def _get_base_selectedSegments(self): selected = tuple([normalizers.normalizeSegment(segment) for segment in self._get_selectedSegments()]) return selected def _get_selectedSegments(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.segments) def _set_base_selectedSegments(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeSegmentIndex(i) else: i = normalizers.normalizeSegment(i) normalized.append(i) self._set_selectedSegments(normalized) def _set_selectedSegments(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.segments, value) # points selectedPoints = dynamicProperty( "base_selectedPoints", """ A list of points selected in the contour. Getting selected point objects: >>> for point in contour.selectedPoints: ... point.move((10, 20)) Setting selected point objects: >>> contour.selectedPoints = somePoints Setting also supports point indexes: >>> contour.selectedPoints = [0, 2] """ ) def _get_base_selectedPoints(self): selected = tuple([normalizers.normalizePoint(point) for point in self._get_selectedPoints()]) return selected def _get_selectedPoints(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.points) def _set_base_selectedPoints(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizePointIndex(i) else: i = normalizers.normalizePoint(i) normalized.append(i) self._set_selectedPoints(normalized) def _set_selectedPoints(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.points, value) # bPoints selectedBPoints = dynamicProperty( "base_selectedBPoints", """ A list of bPoints selected in the contour. Getting selected bPoint objects: >>> for bPoint in contour.selectedBPoints: ... bPoint.move((10, 20)) Setting selected bPoint objects: >>> contour.selectedBPoints = someBPoints Setting also supports bPoint indexes: >>> contour.selectedBPoints = [0, 2] """ ) def _get_base_selectedBPoints(self): selected = tuple([normalizers.normalizeBPoint(bPoint) for bPoint in self._get_selectedBPoints()]) return selected def _get_selectedBPoints(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.bPoints) def _set_base_selectedBPoints(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeBPointIndex(i) else: i = normalizers.normalizeBPoint(i) normalized.append(i) self._set_selectedBPoints(normalized) def _set_selectedBPoints(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.bPoints, value) robotools-fontParts-26e8b8c/Lib/fontParts/base/deprecated.py000066400000000000000000000443121477533125200242240ustar00rootroot00000000000000import warnings # A collection of deprecated roboFab methods. # Those methods are added to keep scripts and code compatible. class RemovedError(Exception): """Exception for things removed from FontParts that were in RoboFab""" # ======== # = base = # ======== class RemovedBase(object): def setParent(self, parent): objName = self.__class__.__name__.replace("Removed", "") raise RemovedError("'%s.setParent()'" % objName) class DeprecatedBase(object): def update(self): objName = self.__class__.__name__.replace("Deprecated", "") warnings.warn("'%s.update': use %s.changed()" % (objName, objName), DeprecationWarning) self.changed() def setChanged(self): objName = self.__class__.__name__.replace("Deprecated", "") warnings.warn("'%s.setChanged': use %s.changed()" % (objName, objName), DeprecationWarning) self.changed() # ================== # = transformation = # ================== class DeprecatedTransformation(object): def move(self, *args, **kwargs): objName = self.__class__.__name__.replace("Deprecated", "") warnings.warn("'%s.move()': use %s.moveBy()" % (objName, objName), DeprecationWarning) self.moveBy(*args, **kwargs) def translate(self, *args, **kwargs): objName = self.__class__.__name__.replace("Deprecated", "") warnings.warn("'%s.translate()': use %s.moveBy()" % (objName, objName), DeprecationWarning) self.moveBy(*args, **kwargs) def scale(self, *args, **kwargs): objName = self.__class__.__name__.replace("Deprecated", "") warnings.warn("'%s.scale()': use %s.scaleBy()" % (objName, objName), DeprecationWarning) if "center" in kwargs: kwargs["origin"] = kwargs["center"] del kwargs["center"] self.scaleBy(*args, **kwargs) def rotate(self, *args, **kwargs): objName = self.__class__.__name__.replace("Deprecated", "") warnings.warn("'%s.rotate()': use %s.rotateBy()" % (objName, objName), DeprecationWarning) if "offset" in kwargs: kwargs["origin"] = kwargs["offset"] del kwargs["offset"] self.rotateBy(*args, **kwargs) def transform(self, *args, **kwargs): objName = self.__class__.__name__.replace("Deprecated", "") warnings.warn("'%s.transform()': use %s.transformBy()" % (objName, objName), DeprecationWarning) self.transformBy(*args, **kwargs) def skew(self, *args, **kwargs): objName = self.__class__.__name__.replace("Deprecated", "") warnings.warn("'%s.skew()': use %s.skewBy()" % (objName, objName), DeprecationWarning) if "offset" in kwargs: kwargs["origin"] = kwargs["offset"] del kwargs["offset"] self.skewBy(*args, **kwargs) # ========= # = Point = # ========= class RemovedPoint(RemovedBase): @staticmethod def select(state=True): raise RemovedError("'Point.select'") class DeprecatedPoint(DeprecatedBase, DeprecatedTransformation): def _generateIdentifier(self): warnings.warn("'Point._generateIdentifier()': use 'Point._getIdentifier()'", DeprecationWarning) return self._getIdentifier() def generateIdentifier(self): warnings.warn("'Point.generateIdentifier()': use 'Point.getIdentifier()'", DeprecationWarning) return self.getIdentifier() def getParent(self): warnings.warn("'Point.getParent()': use 'Point.contour'", DeprecationWarning) return self.contour # ========== # = BPoint = # ========== class RemovedBPoint(RemovedBase): @staticmethod def select(state=True): raise RemovedError("'BPoint.select'") class DeprecatedBPoint(DeprecatedBase, DeprecatedTransformation): def _generateIdentifier(self): warnings.warn("'BPoint._generateIdentifier()': use 'BPoint._getIdentifier()'", DeprecationWarning) return self._getIdentifier() def generateIdentifier(self): warnings.warn("'BPoint.generateIdentifier()': use 'BPoint.getIdentifier()'", DeprecationWarning) return self.getIdentifier() def getParent(self): warnings.warn("'BPoint.getParent()': use 'BPoint.contour'", DeprecationWarning) return self.contour # ========== # = Anchor = # ========== class RemovedAnchor(RemovedBase): @staticmethod def draw(pen): raise RemovedError("'Anchor.draw': UFO3 is not drawing anchors into pens") @staticmethod def drawPoints(pen): raise RemovedError(("'Anchor.drawPoints': UFO3 is not drawing " "anchors into point pens")) class DeprecatedAnchor(DeprecatedBase, DeprecatedTransformation): def _generateIdentifier(self): warnings.warn("'Anchor._generateIdentifier()': use 'Anchor._getIdentifier()'", DeprecationWarning) return self._getIdentifier() def generateIdentifier(self): warnings.warn("'Anchor.generateIdentifier()': use 'Anchor.getIdentifier()'", DeprecationWarning) return self.getIdentifier() def getParent(self): warnings.warn("'Anchor.getParent()': use 'Anchor.glyph'", DeprecationWarning) return self.glyph # ============= # = Component = # ============= class RemovedComponent(RemovedBase): pass class DeprecatedComponent(DeprecatedBase): def _get_box(self): warnings.warn("'Component.box': use Component.bounds", DeprecationWarning) return self.bounds box = property(_get_box, doc="Deprecated Component.box") def _generateIdentifier(self): warnings.warn(("'Component._generateIdentifier()': use " "'Component._getIdentifier()'"), DeprecationWarning) return self._getIdentifier() def generateIdentifier(self): warnings.warn(("'Component.generateIdentifier()': " "use 'Component.getIdentifier()'"), DeprecationWarning) return self.getIdentifier() def getParent(self): warnings.warn("'Component.getParent()': use 'Component.glyph'", DeprecationWarning) return self.glyph def move(self, *args, **kwargs): warnings.warn("'Component.move()': use Component.moveBy()", DeprecationWarning) self.moveBy(*args, **kwargs) def translate(self, *args, **kwargs): warnings.warn("'Component.translate()': use Component.moveBy()", DeprecationWarning) self.moveBy(*args, **kwargs) def rotate(self, *args, **kwargs): warnings.warn("'Component.rotate()': use Component.rotateBy()", DeprecationWarning) if "offset" in kwargs: kwargs["origin"] = kwargs["offset"] del kwargs["offset"] self.rotateBy(*args, **kwargs) def transform(self, *args, **kwargs): warnings.warn("'Component.transform()': use Component.transformBy()", DeprecationWarning) self.transformBy(*args, **kwargs) def skew(self, *args, **kwargs): warnings.warn("'Component.skew()': use Component.skewBy()", DeprecationWarning) if "offset" in kwargs: kwargs["origin"] = kwargs["offset"] del kwargs["offset"] self.skewBy(*args, **kwargs) # =========== # = Segment = # =========== class RemovedSegment(RemovedBase): @staticmethod def insertPoint(point): raise RemovedError("Segment.insertPoint()") @staticmethod def removePoint(point): raise RemovedError("Segment.removePoint()") class DeprecatedSegment(DeprecatedBase, DeprecatedTransformation): def getParent(self): warnings.warn("'Segment.getParent()': use 'Segment.contour'", DeprecationWarning) return self.contour # =========== # = Contour = # =========== class RemovedContour(RemovedBase): pass class DeprecatedContour(DeprecatedBase, DeprecatedTransformation): def _get_box(self): warnings.warn("'Contour.box': use Contour.bounds", DeprecationWarning) return self.bounds box = property(_get_box, doc="Deprecated Contour.box") def reverseContour(self): warnings.warn("'Contour.reverseContour()': use 'Contour.reverse()'", DeprecationWarning) self.reverse() def _generateIdentifier(self): warnings.warn("'Contour._generateIdentifier()': use 'Contour._getIdentifier()'", DeprecationWarning) return self._getIdentifier() def generateIdentifier(self): warnings.warn("'Contour.generateIdentifier()': use 'Contour.getIdentifier()'", DeprecationWarning) return self.getIdentifier() def _generateIdentifierforPoint(self, point): warnings.warn(("'Contour._generateIdentifierforPoint()': use " "'Contour._getIdentifierforPoint()'"), DeprecationWarning) return self._getIdentifierforPoint(point) def generateIdentifierforPoint(self, point): warnings.warn(("'Contour.generateIdentifierforPoint()': use " "'Contour.getIdentifierForPoint()'"), DeprecationWarning) return self.getIdentifierForPoint(point) def getParent(self): warnings.warn("'Contour.getParent()': use 'Contour.glyph'", DeprecationWarning) return self.glyph # ========= # = Glyph = # ========= class RemovedGlyph(RemovedBase): @staticmethod def center(padding=None): raise RemovedError("'Glyph.center()'") @staticmethod def clearVGuides(): raise RemovedError("'Glyph.clearVGuides()': use Glyph.clearGuidelines()") @staticmethod def clearHGuides(): raise RemovedError("'Glyph.clearHGuides()': use Glyph.clearGuidelines()") class DeprecatedGlyph(DeprecatedBase, DeprecatedTransformation): def _get_mark(self): warnings.warn("'Glyph.mark': use Glyph.markColor", DeprecationWarning) return self.markColor def _set_mark(self, value): warnings.warn("'Glyph.mark': use Glyph.markColor", DeprecationWarning) self.markColor = value mark = property(_get_mark, _set_mark, doc="Deprecated Mark color") def _get_box(self): warnings.warn("'Glyph.box': use Glyph.bounds", DeprecationWarning) return self.bounds box = property(_get_box, doc="Deprecated Glyph.box") def getAnchors(self): warnings.warn("'Glyph.getAnchors()': use Glyph.anchors", DeprecationWarning) return self.anchors def getComponents(self): warnings.warn("'Glyph.getComponents()': use Glyph.components", DeprecationWarning) return self.components def getParent(self): warnings.warn("'Glyph.getParent()': use 'Glyph.font'", DeprecationWarning) return self.font def readGlyphFromString(self, glifData): warnings.warn(("'Glyph.readGlyphFromString()': use " "'Glyph.loadFromGLIF()'"), DeprecationWarning) return self.loadFromGLIF(glifData) def writeGlyphToString(self, glyphFormatVersion=2): warnings.warn(("'Glyph.writeGlyphToString()': use " "'Glyph.dumpToGLIF()'"), DeprecationWarning) return self.dumpToGLIF(glyphFormatVersion) # ============= # = Guideline = # ============= class RemovedGuideline(RemovedBase): pass class DeprecatedGuideline(DeprecatedBase, DeprecatedTransformation): def _generateIdentifier(self): warnings.warn(("'Guideline._generateIdentifier()': " "use 'Guideline._getIdentifier()'"), DeprecationWarning) return self._getIdentifier() def generateIdentifier(self): warnings.warn(("'Guideline.generateIdentifier()': " "use 'Guideline.getIdentifier()'"), DeprecationWarning) return self.getIdentifier() def getParent(self): warnings.warn(("'Guideline.getParent()': use 'Guideline.glyph'" " or 'Guideline.font'"), DeprecationWarning) glyph = self.glyph if glyph is not None: return glyph return self.font # ======= # = Lib = # ======= class RemovedLib(RemovedBase): pass class DeprecatedLib(object): def getParent(self): warnings.warn("'Lib.getParent()': use 'Lib.glyph' or 'Lib.font'", DeprecationWarning) glyph = self.glyph if glyph is not None: return glyph return self.font def setChanged(self): warnings.warn("'Lib.setChanged': use Lib.changed()", DeprecationWarning) self.changed() # ========== # = Groups = # ========== class RemovedGroups(RemovedBase): pass class DeprecatedGroups(object): def getParent(self): warnings.warn("'Groups.getParent()': use 'Groups.font'", DeprecationWarning) return self.font def setChanged(self): warnings.warn("'Groups.setChanged': use Groups.changed()", DeprecationWarning) self.changed() # =========== # = Kerning = # =========== class RemovedKerning(object): @staticmethod def setParent(parent): raise RemovedError("'Kerning.setParent()'") @staticmethod def swapNames(swaptable): raise RemovedError("Kerning.swapNames()") @staticmethod def getLeft(glyphName): raise RemovedError("Kerning.getLeft()") @staticmethod def getRight(glyphName): raise RemovedError("Kerning.getRight()") @staticmethod def getExtremes(): raise RemovedError("Kerning.getExtremes()") @staticmethod def add(value): raise RemovedError("Kerning.add()") @staticmethod def minimize(minimum=10): raise RemovedError("Kerning.minimize()") @staticmethod def importAFM(path, clearExisting=True): raise RemovedError("Kerning.importAFM()") @staticmethod def getAverage(): raise RemovedError("Kerning.getAverage()") @staticmethod def combine(kerningDicts, overwriteExisting=True): raise RemovedError("Kerning.combine()") @staticmethod def eliminate(leftGlyphsToEliminate=None, rightGlyphsToEliminate=None, analyzeOnly=False): raise RemovedError("Kerning.eliminate()") @staticmethod def occurrenceCount(glyphsToCount): raise RemovedError("Kerning.occurrenceCount()") @staticmethod def implodeClasses(leftClassDict=None, rightClassDict=None, analyzeOnly=False): raise RemovedError("Kerning.implodeClasses()") @staticmethod def explodeClasses(leftClassDict=None, rightClassDict=None, analyzeOnly=False): raise RemovedError("Kerning.explodeClasses()") class DeprecatedKerning(object): def setChanged(self): warnings.warn("'Kerning.setChanged': use Kerning.changed()", DeprecationWarning) self.changed() def getParent(self): warnings.warn("'Kerning.getParent()': use 'Kerning.font'", DeprecationWarning) return self.font # ======== # = Info = # ======== class RemovedInfo(RemovedBase): pass class DeprecatedInfo(DeprecatedBase): def getParent(self): warnings.warn("'Info.getParent()': use 'Info.font'", DeprecationWarning) return self.font # ========= # = Image = # ========= class RemovedImage(RemovedBase): pass class DeprecatedImage(DeprecatedBase): def getParent(self): warnings.warn("'Image.getParent()': use 'Image.glyph'", DeprecationWarning) return self.glyph # ============ # = Features = # ============ class RemovedFeatures(RemovedBase): @staticmethod def round(): raise RemovedError("'Features.round()'") class DeprecatedFeatures(DeprecatedBase): def getParent(self): warnings.warn("'Features.getParent()': use 'Features.font'", DeprecationWarning) return self.font # ========= # = Layer = # ========= class RemovedLayer(RemovedBase): pass class DeprecatedLayer(DeprecatedBase): def getParent(self): warnings.warn("'Layer.getParent()': use 'Layer.font'", DeprecationWarning) return self.font # ======== # = Font = # ======== class RemovedFont(RemovedBase): @staticmethod def getParent(): raise RemovedError("'Font.getParent()'") @staticmethod def generateGlyph(*args, **kwargs): raise RemovedError("'Font.generateGlyph()'") @staticmethod def compileGlyph(*args, **kwargs): raise RemovedError("'Font.compileGlyph()'") @staticmethod def getGlyphNameToFileNameFunc(): raise RemovedError("'Font.getGlyphNameToFileNameFunc()'") class DeprecatedFont(DeprecatedBase): def _get_fileName(self): warnings.warn("'Font.fileName': use os.path.basename(Font.path)", DeprecationWarning) return self.path fileName = property(_get_fileName, doc="Deprecated Font.fileName") def getWidth(self, glyphName): warnings.warn("'Font.getWidth(): use Font[glyphName].width'", DeprecationWarning) return self[glyphName].width def getGlyph(self, glyphName): warnings.warn("'Font.getGlyph(): use Font[glyphName]'", DeprecationWarning) return self[glyphName] def _get_selection(self): warnings.warn("'Font.selection: use Font.selectedGlyphNames'", DeprecationWarning) return self.selectedGlyphNames def _set_selection(self, glyphNames): warnings.warn("'Font.selection: use Font.selectedGlyphNames'", DeprecationWarning) self.selectedGlyphNames = glyphNames selection = property(_get_selection, _set_selection, doc="Deprecated Font.selection") robotools-fontParts-26e8b8c/Lib/fontParts/base/errors.py000066400000000000000000000001561477533125200234360ustar00rootroot00000000000000# ------------------- # Universal Exception # ------------------- class FontPartsError(Exception): pass robotools-fontParts-26e8b8c/Lib/fontParts/base/features.py000066400000000000000000000042371477533125200237440ustar00rootroot00000000000000from fontParts.base.base import BaseObject, dynamicProperty, reference from fontParts.base import normalizers from fontParts.base.deprecated import DeprecatedFeatures, RemovedFeatures class BaseFeatures(BaseObject, DeprecatedFeatures, RemovedFeatures): copyAttributes = ("text",) def _reprContents(self): contents = [] if self.font is not None: contents.append("for font") contents += self.font._reprContents() return contents # ------- # Parents # ------- # Font _font = None font = dynamicProperty("font", "The features' parent :class:`BaseFont`.") def _get_font(self): if self._font is None: return None return self._font() def _set_font(self, font): if self._font is not None and self._font() != font: raise AssertionError("font for features already set and is not same as font") if font is not None: font = reference(font) self._font = font # ---- # Text # ---- text = dynamicProperty( "base_text", """ The `.fea formated `_ text representing the features. It must be a :ref:`type-string`. """ ) def _get_base_text(self): value = self._get_text() if value is not None: value = normalizers.normalizeFeatureText(value) return value def _set_base_text(self, value): if value is not None: value = normalizers.normalizeFeatureText(value) self._set_text(value) def _get_text(self): """ This is the environment implementation of :attr:`BaseFeatures.text`. This must return a :ref:`type-string`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_text(self, value): """ This is the environment implementation of :attr:`BaseFeatures.text`. **value** will be a :ref:`type-string`. Subclasses must override this method. """ self.raiseNotImplementedError() robotools-fontParts-26e8b8c/Lib/fontParts/base/font.py000066400000000000000000001646451477533125200231060ustar00rootroot00000000000000import os from fontTools import ufoLib from fontParts.base.errors import FontPartsError from fontParts.base.base import dynamicProperty, InterpolationMixin from fontParts.base.layer import _BaseGlyphVendor from fontParts.base import normalizers from fontParts.base.compatibility import FontCompatibilityReporter from fontParts.base.deprecated import DeprecatedFont, RemovedFont class BaseFont( _BaseGlyphVendor, InterpolationMixin, DeprecatedFont, RemovedFont ): """ A font object. This object is almost always created with one of the font functions in :ref:`fontparts-world`. """ def __init__(self, pathOrObject=None, showInterface=True): """ When constructing a font, the object can be created in a new file, from an existing file or from a native object. This is defined with the **pathOrObjectArgument**. If **pathOrObject** is a string, the string must represent an existing file. If **pathOrObject** is an instance of the environment's unwrapped native font object, wrap it with FontParts. If **pathOrObject** is None, create a new, empty font. If **showInterface** is ``False``, the font should be created without graphical interface. The default for **showInterface** is ``True``. """ super(BaseFont, self).__init__(pathOrObject=pathOrObject, showInterface=showInterface) def _reprContents(self): contents = [ "'%s %s'" % (self.info.familyName, self.info.styleName), ] if self.path is not None: contents.append("path=%r" % self.path) return contents # ---- # Copy # ---- copyAttributes = ( "info", "groups", "kerning", "features", "lib", "layerOrder", "defaultLayerName", "glyphOrder" ) def copy(self): """ Copy the font into a new font. :: >>> copiedFont = font.copy() This will copy: * info * groups * kerning * features * lib * layers * layerOrder * defaultLayerName * glyphOrder * guidelines """ return super(BaseFont, self).copy() def copyData(self, source): """ Copy data from **source** into this font. Refer to :meth:`BaseFont.copy` for a list of values that will be copied. """ # set the default layer name self.defaultLayer.name = source.defaultLayerName for layerName in source.layerOrder: if layerName in self.layerOrder: layer = self.getLayer(layerName) else: layer = self.newLayer(layerName) layer.copyData(source.getLayer(layerName)) for guideline in self.guidelines: self.appendGuideline(guideline) super(BaseFont, self).copyData(source) # --------------- # File Operations # --------------- # Initialize def _init(self, pathOrObject=None, showInterface=True, **kwargs): """ Initialize this object. This should wrap a native font object based on the values for **pathOrObject**: +--------------------+---------------------------------------------------+ | None | Create a new font. | +--------------------+---------------------------------------------------+ | string | Open the font file located at the given location. | +--------------------+---------------------------------------------------+ | native font object | Wrap the given object. | +--------------------+---------------------------------------------------+ If **showInterface** is ``False``, the font should be created without graphical interface. Subclasses must override this method. """ self.raiseNotImplementedError() # path path = dynamicProperty( "base_path", """ The path to the file this object represents. :: >>> print font.path "/path/to/my/font.ufo" """ ) def _get_base_path(self): path = self._get_path() if path is not None: path = normalizers.normalizeFilePath(path) return path def _get_path(self, **kwargs): """ This is the environment implementation of :attr:`BaseFont.path`. This must return a :ref:`type-string` defining the location of the file or ``None`` indicating that the font does not have a file representation. If the returned value is not ``None`` it will be normalized with :func:`normalizers.normalizeFilePath`. Subclasses must override this method. """ self.raiseNotImplementedError() # save def save(self, path=None, showProgress=False, formatVersion=None, fileStructure=None): """ Save the font to **path**. >>> font.save() >>> font.save("/path/to/my/font-2.ufo") If **path** is None, use the font's original location. The file type must be inferred from the file extension of the given path. If no file extension is given, the environment may fall back to the format of its choice. **showProgress** indicates if a progress indicator should be displayed during the operation. Environments may or may not implement this behavior. **formatVersion** indicates the format version that should be used for writing the given file type. For example, if 2 is given for formatVersion and the file type being written if UFO, the file is to be written in UFO 2 format. This value is not limited to UFO format versions. If no format version is given, the original format version of the file should be preserved. If there is no original format version it is implied that the format version is the latest version for the file type as supported by the environment. **fileStructure** indicates the file structure of the written ufo. The **fileStructure** can either be None, 'zip' or 'package', None will use the existing file strucure or the default one for unsaved font. 'package' is the default file structure and 'zip' will save the font to .ufoz. .. note:: Environments may define their own rules governing when a file should be saved into its original location and when it should not. For example, a font opened from a compiled OpenType font may not be written back into the original OpenType font. """ if path is None and self.path is None: raise IOError(("The font cannot be saved because no file " "location has been given.")) if path is not None: path = normalizers.normalizeFilePath(path) showProgress = bool(showProgress) if formatVersion is not None: formatVersion = normalizers.normalizeFileFormatVersion( formatVersion) if fileStructure is not None: fileStructure = normalizers.normalizeFileStructure(fileStructure) self._save(path=path, showProgress=showProgress, formatVersion=formatVersion, fileStructure=fileStructure) def _save(self, path=None, showProgress=False, formatVersion=None, fileStructure=None, **kwargs): """ This is the environment implementation of :meth:`BaseFont.save`. **path** will be a :ref:`type-string` or ``None``. If **path** is not ``None``, the value will have been normalized with :func:`normalizers.normalizeFilePath`. **showProgress** will be a ``bool`` indicating if the environment should display a progress bar during the operation. Environments are not *required* to display a progress bar even if **showProgess** is ``True``. **formatVersion** will be :ref:`type-int-float` or ``None`` indicating the file format version to write the data into. It will have been normalized with :func:`normalizers.normalizeFileFormatVersion`. Subclasses must override this method. """ self.raiseNotImplementedError() # close def close(self, save=False): """ Close the font. >>> font.close() **save** is a boolean indicating if the font should be saved prior to closing. If **save** is ``True``, the :meth:`BaseFont.save` method will be called. The default is ``False``. """ if save: self.save() self._close() def _close(self, **kwargs): """ This is the environment implementation of :meth:`BaseFont.close`. Subclasses must override this method. """ self.raiseNotImplementedError() # generate @staticmethod def generateFormatToExtension(format, fallbackFormat): """ +--------------+--------------------------------------------------------------------+ | mactype1 | Mac Type 1 font (generates suitcase and LWFN file) | +--------------+--------------------------------------------------------------------+ | macttf | Mac TrueType font (generates suitcase) | +--------------+--------------------------------------------------------------------+ | macttdfont | Mac TrueType font (generates suitcase with resources in data fork) | +--------------+--------------------------------------------------------------------+ | otfcff | PS OpenType (CFF-based) font (OTF) | +--------------+--------------------------------------------------------------------+ | otfttf | PC TrueType/TT OpenType font (TTF) | +--------------+--------------------------------------------------------------------+ | pctype1 | PC Type 1 font (binary/PFB) | +--------------+--------------------------------------------------------------------+ | pcmm | PC MultipleMaster font (PFB) | +--------------+--------------------------------------------------------------------+ | pctype1ascii | PC Type 1 font (ASCII/PFA) | +--------------+--------------------------------------------------------------------+ | pcmmascii | PC MultipleMaster font (ASCII/PFA) | +--------------+--------------------------------------------------------------------+ | ufo1 | UFO format version 1 | +--------------+--------------------------------------------------------------------+ | ufo2 | UFO format version 2 | +--------------+--------------------------------------------------------------------+ | ufo3 | UFO format version 3 | +--------------+--------------------------------------------------------------------+ | unixascii | UNIX ASCII font (ASCII/PFA) | +--------------+--------------------------------------------------------------------+ """ formatToExtension = dict( # mactype1=None, macttf=".ttf", macttdfont=".dfont", otfcff=".otf", otfttf=".ttf", # pctype1=None, # pcmm=None, # pctype1ascii=None, # pcmmascii=None, ufo1=".ufo", ufo2=".ufo", ufo3=".ufo", unixascii=".pfa", ) return formatToExtension.get(format, fallbackFormat) def generate(self, format, path=None, **environmentOptions): """ Generate the font to another format. >>> font.generate("otfcff") >>> font.generate("otfcff", "/path/to/my/font.otf") **format** defines the file format to output. Standard format identifiers can be found in :attr:`BaseFont.generateFormatToExtension`: Environments are not required to support all of these and environments may define their own format types. **path** defines the location where the new file should be created. If a file already exists at that location, it will be overwritten by the new file. If **path** defines a directory, the file will be output as the current file name, with the appropriate suffix for the format, into the given directory. If no **path** is given, the file will be output into the same directory as the source font with the file named with the current file name, with the appropriate suffix for the format. Environments may allow unique keyword arguments in this method. For example, if a tool allows decomposing components during a generate routine it may allow this: >>> font.generate("otfcff", "/p/f.otf", decompose=True) """ import warnings if format is None: raise ValueError("The format must be defined when generating.") elif not isinstance(format, str): raise TypeError("The format must be defined as a string.") env = {} for key, value in environmentOptions.items(): valid = self._isValidGenerateEnvironmentOption(key) if not valid: warnings.warn("The %s argument is not supported " "in this environment." % key, UserWarning) env[key] = value environmentOptions = env ext = self.generateFormatToExtension(format, "." + format) if path is None and self.path is None: raise IOError(("The file cannot be generated because an " "output path was not defined.")) elif path is None: path = os.path.splitext(self.path)[0] path += ext elif os.path.isdir(path): if self.path is None: raise IOError(("The file cannot be generated because " "the file does not have a path.")) fileName = os.path.basename(self.path) fileName += ext path = os.path.join(path, fileName) path = normalizers.normalizeFilePath(path) return self._generate( format=format, path=path, environmentOptions=environmentOptions ) @staticmethod def _isValidGenerateEnvironmentOption(name): """ Any unknown keyword arguments given to :meth:`BaseFont.generate` will be passed to this method. **name** will be the name used for the argument. Environments may evaluate if **name** is a supported option. If it is, they must return `True` if it is not, they must return `False`. Subclasses may override this method. """ return False def _generate(self, format, path, environmentOptions, **kwargs): """ This is the environment implementation of :meth:`BaseFont.generate`. **format** will be a :ref:`type-string` defining the output format. Refer to the :meth:`BaseFont.generate` documentation for the standard format identifiers. If the value given for **format** is not supported by the environment, the environment must raise :exc:`FontPartsError`. **path** will be a :ref:`type-string` defining the location where the file should be created. It will have been normalized with :func:`normalizers.normalizeFilePath`. **environmentOptions** will be a dictionary of names validated with :meth:`BaseFont._isValidGenerateEnvironmentOption` nd the given values. These values will not have been passed through any normalization functions. Subclasses must override this method. """ self.raiseNotImplementedError() # ----------- # Sub-Objects # ----------- # info info = dynamicProperty( "base_info", """ The font's :class:`BaseInfo` object. >>> font.info.familyName "My Family" """ ) def _get_base_info(self): info = self._get_info() info.font = self return info def _get_info(self): """ This is the environment implementation of :attr:`BaseFont.info`. This must return an instance of a :class:`BaseInfo` subclass. Subclasses must override this method. """ self.raiseNotImplementedError() # groups groups = dynamicProperty( "base_groups", """ The font's :class:`BaseGroups` object. >>> font.groups["myGroup"] ["A", "B", "C"] """ ) def _get_base_groups(self): groups = self._get_groups() groups.font = self return groups def _get_groups(self): """ This is the environment implementation of :attr:`BaseFont.groups`. This must return an instance of a :class:`BaseGroups` subclass. Subclasses must override this method. """ self.raiseNotImplementedError() # kerning kerning = dynamicProperty( "base_kerning", """ The font's :class:`BaseKerning` object. >>> font.kerning["A", "B"] -100 """ ) def _get_base_kerning(self): kerning = self._get_kerning() kerning.font = self return kerning def _get_kerning(self): """ This is the environment implementation of :attr:`BaseFont.kerning`. This must return an instance of a :class:`BaseKerning` subclass. Subclasses must override this method. """ self.raiseNotImplementedError() def getFlatKerning(self): """ Get the font's kerning as a flat dictionary. """ return self._getFlatKerning() def _getFlatKerning(self): """ This is the environment implementation of :meth:`BaseFont.getFlatKerning`. Subclasses may override this method. """ kernOrder = { (True, True): 0, # group group (True, False): 1, # group glyph (False, True): 2, # glyph group (False, False): 3, # glyph glyph } def kerningSortKeyFunc(pair): g1, g2 = pair g1grp = g1.startswith("public.kern1.") g2grp = g2.startswith("public.kern2.") return (kernOrder[g1grp, g2grp], pair) flatKerning = dict() kerning = self.kerning groups = self.groups for pair in sorted(self.kerning.keys(), key=kerningSortKeyFunc): kern = kerning[pair] (left, right) = pair if left.startswith("public.kern1."): left = groups.get(left, []) else: left = [left] if right.startswith("public.kern2."): right = groups.get(right, []) else: right = [right] for r in right: for l in left: flatKerning[(l, r)] = kern return flatKerning # features features = dynamicProperty( "base_features", """ The font's :class:`BaseFeatures` object. >>> font.features.text "include(features/substitutions.fea);" """ ) def _get_base_features(self): features = self._get_features() features.font = self return features def _get_features(self): """ This is the environment implementation of :attr:`BaseFont.features`. This must return an instance of a :class:`BaseFeatures` subclass. Subclasses must override this method. """ self.raiseNotImplementedError() # lib lib = dynamicProperty( "base_lib", """ The font's :class:`BaseLib` object. >>> font.lib["org.robofab.hello"] "world" """ ) def _get_base_lib(self): lib = self._get_lib() lib.font = self return lib def _get_lib(self): """ This is the environment implementation of :attr:`BaseFont.lib`. This must return an instance of a :class:`BaseLib` subclass. Subclasses must override this method. """ self.raiseNotImplementedError() # tempLib tempLib = dynamicProperty( "base_tempLib", """ The font's :class:`BaseLib` object. :: >>> font.tempLib["org.robofab.hello"] "world" """ ) def _get_base_tempLib(self): lib = self._get_tempLib() lib.font = self return lib def _get_tempLib(self): """ This is the environment implementation of :attr:`BaseLayer.tempLib`. This must return an instance of a :class:`BaseLib` subclass. """ self.raiseNotImplementedError() # ----------------- # Layer Interaction # ----------------- layers = dynamicProperty( "base_layers", """ The font's :class:`BaseLayer` objects. >>> for layer in font.layers: ... layer.name "My Layer 1" "My Layer 2" """ ) def _get_base_layers(self): layers = self._get_layers() for layer in layers: self._setFontInLayer(layer) return tuple(layers) def _get_layers(self, **kwargs): """ This is the environment implementation of :attr:`BaseFont.layers`. This must return an :ref:`type-immutable-list` containing instances of :class:`BaseLayer` subclasses. The items in the list should be in the order defined by :attr:`BaseFont.layerOrder`. Subclasses must override this method. """ self.raiseNotImplementedError() # order layerOrder = dynamicProperty( "base_layerOrder", """ A list of layer names indicating order of the layers in the font. >>> font.layerOrder = ["My Layer 2", "My Layer 1"] >>> font.layerOrder ["My Layer 2", "My Layer 1"] """ ) def _get_base_layerOrder(self): value = self._get_layerOrder() value = normalizers.normalizeLayerOrder(value, self) return list(value) def _set_base_layerOrder(self, value): value = normalizers.normalizeLayerOrder(value, self) self._set_layerOrder(value) def _get_layerOrder(self, **kwargs): """ This is the environment implementation of :attr:`BaseFont.layerOrder`. This must return an :ref:`type-immutable-list` defining the order of the layers in the font. The contents of the list must be layer names as :ref:`type-string`. The list will be normalized with :func:`normalizers.normalizeLayerOrder`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_layerOrder(self, value, **kwargs): """ This is the environment implementation of :attr:`BaseFont.layerOrder`. **value** will be a **list** of :ref:`type-string` representing layer names. The list will have been normalized with :func:`normalizers.normalizeLayerOrder`. Subclasses must override this method. """ self.raiseNotImplementedError() # default layer def _setFontInLayer(self, layer): if layer.font is None: layer.font = self defaultLayerName = dynamicProperty( "base_defaultLayerName", """ The name of the font's default layer. >>> font.defaultLayerName = "My Layer 2" >>> font.defaultLayerName "My Layer 2" """ ) def _get_base_defaultLayerName(self): value = self._get_defaultLayerName() value = normalizers.normalizeDefaultLayerName(value, self) return value def _set_base_defaultLayerName(self, value): value = normalizers.normalizeDefaultLayerName(value, self) self._set_defaultLayerName(value) def _get_defaultLayerName(self): """ This is the environment implementation of :attr:`BaseFont.defaultLayerName`. Return the name of the default layer as a :ref:`type-string`. The name will be normalized with :func:`normalizers.normalizeDefaultLayerName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_defaultLayerName(self, value, **kwargs): """ This is the environment implementation of :attr:`BaseFont.defaultLayerName`. **value** will be a :ref:`type-string`. It will have been normalized with :func:`normalizers.normalizeDefaultLayerName`. Subclasses must override this method. """ self.raiseNotImplementedError() defaultLayer = dynamicProperty( "base_defaultLayer", """ The font's default layer. >>> layer = font.defaultLayer >>> font.defaultLayer = otherLayer """ ) def _get_defaultLayer(self): layer = self._get_base_defaultLayer() layer = normalizers.normalizeLayer(layer) return layer def _set_defaultLayer(self, layer): layer = normalizers.normalizeLayer(layer) self._set_base_defaultLayer(layer) def _get_base_defaultLayer(self): """ This is the environment implementation of :attr:`BaseFont.defaultLayer`. Return the default layer as a :class:`BaseLayer` object. The layer will be normalized with :func:`normalizers.normalizeLayer`. Subclasses must override this method. """ name = self.defaultLayerName layer = self.getLayer(name) return layer def _set_base_defaultLayer(self, value): """ This is the environment implementation of :attr:`BaseFont.defaultLayer`. **value** will be a :class:`BaseLayer`. It will have been normalized with :func:`normalizers.normalizeLayer`. Subclasses must override this method. """ self.defaultLayerName = value.name # get def getLayer(self, name): """ Get the :class:`BaseLayer` with **name**. >>> layer = font.getLayer("My Layer 2") """ name = normalizers.normalizeLayerName(name) if name not in self.layerOrder: raise ValueError("No layer with the name '%s' exists." % name) layer = self._getLayer(name) self._setFontInLayer(layer) return layer def _getLayer(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.getLayer`. **name** will be a :ref:`type-string`. It will have been normalized with :func:`normalizers.normalizeLayerName` and it will have been verified as an existing layer. This must return an instance of :class:`BaseLayer`. Subclasses may override this method. """ for layer in self.layers: if layer.name == name: return layer # new def newLayer(self, name, color=None): """ Make a new layer with **name** and **color**. **name** must be a :ref:`type-string` and **color** must be a :ref:`type-color` or ``None``. >>> layer = font.newLayer("My Layer 3") The will return the newly created :class:`BaseLayer`. """ name = normalizers.normalizeLayerName(name) if name in self.layerOrder: layer = self.getLayer(name) if color is not None: layer.color = color return layer if color is not None: color = normalizers.normalizeColor(color) layer = self._newLayer(name=name, color=color) self._setFontInLayer(layer) return layer def _newLayer(self, name, color, **kwargs): """ This is the environment implementation of :meth:`BaseFont.newLayer`. **name** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **name** will not be the same as the name of an existing layer. **color** will be a :ref:`type-color` or ``None``. If the value is not ``None`` the value will have been normalized with :func:`normalizers.normalizeColor`. This must return an instance of a :class:`BaseLayer` subclass that represents the new layer. Subclasses must override this method. """ self.raiseNotImplementedError() # remove def removeLayer(self, name): """ Remove the layer with **name** from the font. >>> font.removeLayer("My Layer 3") """ name = normalizers.normalizeLayerName(name) if name not in self.layerOrder: raise ValueError("No layer with the name '%s' exists." % name) self._removeLayer(name) def _removeLayer(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.removeLayer`. **name** will be a :ref:`type-string` defining the name of an existing layer. The value will have been normalized with :func:`normalizers.normalizeLayerName`. Subclasses must override this method. """ self.raiseNotImplementedError() # insert def insertLayer(self, layer, name=None): """ Insert **layer** into the font. :: >>> layer = font.insertLayer(otherLayer, name="layer 2") This will not insert the layer directly. Rather, a new layer will be created and the data from **layer** will be copied to to the new layer. **name** indicates the name that should be assigned to the layer after insertion. If **name** is not given, the layer's original name must be used. If the layer does not have a name, an error must be raised. The data that will be inserted from **layer** is the same data as documented in :meth:`BaseLayer.copy`. """ if name is None: name = layer.name name = normalizers.normalizeLayerName(name) if name in self: self.removeLayer(name) return self._insertLayer(layer, name=name) def _insertLayer(self, layer, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.insertLayer`. This must return an instance of a :class:`BaseLayer` subclass. **layer** will be a layer object with the attributes necessary for copying as defined in :meth:`BaseLayer.copy` An environment must not insert **layer** directly. Instead the data from **layer** should be copied to a new layer. **name** will be a :ref:`type-string` representing a glyph layer. It will have been normalized with :func:`normalizers.normalizeLayerName`. **name** will have been tested to make sure that no layer with the same name exists in the font. Subclasses may override this method. """ if name != layer.name and layer.name in self.layerOrder: layer = layer.copy() layer.name = name dest = self.newLayer(name) dest.copyData(layer) return dest # duplicate def duplicateLayer(self, layerName, newLayerName): """ Duplicate the layer with **layerName**, assign **newLayerName** to the new layer and insert the new layer into the font. :: >>> layer = font.duplicateLayer("layer 1", "layer 2") """ layerOrder = self.layerOrder layerName = normalizers.normalizeLayerName(layerName) if layerName not in layerOrder: raise ValueError("No layer with the name '%s' exists." % layerName) newLayerName = normalizers.normalizeLayerName(newLayerName) if newLayerName in layerOrder: raise ValueError("A layer with the name '%s' already exists." % newLayerName) newLayer = self._duplicateLayer(layerName, newLayerName) newLayer = normalizers.normalizeLayer(newLayer) return newLayer def _duplicateLayer(self, layerName, newLayerName): """ This is the environment implementation of :meth:`BaseFont.duplicateLayer`. **layerName** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **layerName** will be a layer that exists in the font. **newLayerName** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **newLayerName** will have been tested to make sure that no layer with the same name exists in the font. This must return an instance of a :class:`BaseLayer` subclass. Subclasses may override this method. """ newLayer = self.getLayer(layerName).copy() return self.insertLayer(newLayer, newLayerName) def swapLayerNames(self, layerName, otherLayerName): """ Assign **layerName** to the layer currently named **otherLayerName** and assign the name **otherLayerName** to the layer currently named **layerName**. >>> font.swapLayerNames("before drawing revisions", "after drawing revisions") """ layerOrder = self.layerOrder layerName = normalizers.normalizeLayerName(layerName) if layerName not in layerOrder: raise ValueError("No layer with the name '%s' exists." % layerName) otherLayerName = normalizers.normalizeLayerName(otherLayerName) if otherLayerName not in layerOrder: raise ValueError("No layer with the name '%s' exists." % otherLayerName) self._swapLayers(layerName, otherLayerName) def _swapLayers(self, layerName, otherLayerName): """ This is the environment implementation of :meth:`BaseFont.swapLayerNames`. **layerName** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **layerName** will be a layer that exists in the font. **otherLayerName** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **otherLayerName** will be a layer that exists in the font. Subclasses may override this method. """ import random layer1 = self.getLayer(layerName) layer2 = self.getLayer(otherLayerName) # make a temporary name and assign it to # the first layer to prevent two layers # from having the same name at once. layerOrder = self.layerOrder for _ in range(50): # shout out to PostScript unique IDs tempLayerName = str(random.randint(4000000, 4999999)) if tempLayerName not in layerOrder: break if tempLayerName in layerOrder: raise FontPartsError(("Couldn't find a temporary layer name " "after 50 tries. Sorry. Please try again.")) layer1.name = tempLayerName # now swap layer2.name = layerName layer1.name = otherLayerName # ----------------- # Glyph Interaction # ----------------- # base implementation overrides def _getItem(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.__getitem__`. **name** will be a :ref:`type-string` defining an existing glyph in the default layer. The value will have been normalized with :func:`normalizers.normalizeGlyphName`. Subclasses may override this method. """ layer = self.defaultLayer return layer[name] def _keys(self, **kwargs): """ This is the environment implementation of :meth:`BaseFont.keys`. This must return an :ref:`type-immutable-list` of all glyph names in the default layer. Subclasses may override this method. """ layer = self.defaultLayer return layer.keys() def _newGlyph(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.newGlyph`. **name** will be a :ref:`type-string` representing a valid glyph name. The value will have been tested to make sure that an existing glyph in the default layer does not have an identical name. The value will have been normalized with :func:`normalizers.normalizeGlyphName`. This must return an instance of :class:`BaseGlyph` representing the new glyph. Subclasses may override this method. """ layer = self.defaultLayer # clear is False here because the base newFont # that has called this method will have already # handled the clearing as specified by the caller. return layer.newGlyph(name, clear=False) def _removeGlyph(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.removeGlyph`. **name** will be a :ref:`type-string` representing an existing glyph in the default layer. The value will have been normalized with :func:`normalizers.normalizeGlyphName`. Subclasses may override this method. """ layer = self.defaultLayer layer.removeGlyph(name) def __setitem__(self, name, glyph): """ Insert **glyph** into the font. :: >>> glyph = font["A"] = otherGlyph This will not insert the glyph directly. Rather, a new glyph will be created and the data from **glyph** will be copied to the new glyph. **name** indicates the name that should be assigned to the glyph after insertion. The data that will be inserted from **glyph** is the same data as documented in :meth:`BaseGlyph.copy`. On a font level **font.glyphOrder** will be preserved if the **name** is already present. """ name = normalizers.normalizeGlyphName(name) if name in self: # clear the glyph here if the glyph exists dest = self._getItem(name) dest.clear() return self._insertGlyph(glyph, name=name, clear=False) # order glyphOrder = dynamicProperty( "base_glyphOrder", """ The preferred order of the glyphs in the font. >>> font.glyphOrder ["C", "B", "A"] >>> font.glyphOrder = ["A", "B", "C"] """ ) def _get_base_glyphOrder(self): value = self._get_glyphOrder() value = normalizers.normalizeGlyphOrder(value) return value def _set_base_glyphOrder(self, value): value = normalizers.normalizeGlyphOrder(value) self._set_glyphOrder(value) def _get_glyphOrder(self): """ This is the environment implementation of :attr:`BaseFont.glyphOrder`. This must return an :ref:`type-immutable-list` containing glyph names representing the glyph order in the font. The value will be normalized with :func:`normalizers.normalizeGlyphOrder`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_glyphOrder(self, value): """ This is the environment implementation of :attr:`BaseFont.glyphOrder`. **value** will be a list of :ref:`type-string`. It will have been normalized with :func:`normalizers.normalizeGlyphOrder`. Subclasses must override this method. """ self.raiseNotImplementedError() # ----------------- # Global Operations # ----------------- def round(self): """ Round all approriate data to integers. >>> font.round() This is the equivalent of calling the round method on: * info * kerning * the default layer * font-level guidelines This applies only to the default layer. """ self._round() def _round(self): """ This is the environment implementation of :meth:`BaseFont.round`. Subclasses may override this method. """ layer = self.defaultLayer layer.round() self.info.round() self.kerning.round() for guideline in self.guidelines: guideline.round() def autoUnicodes(self): """ Use heuristics to set Unicode values in all glyphs. >>> font.autoUnicodes() Environments will define their own heuristics for automatically determining values. This applies only to the default layer. """ self._autoUnicodes() def _autoUnicodes(self): """ This is the environment implementation of :meth:`BaseFont.autoUnicodes`. Subclasses may override this method. """ layer = self.defaultLayer layer.autoUnicodes() # ---------- # Guidelines # ---------- def _setFontInGuideline(self, guideline): if guideline.font is None: guideline.font = self guidelines = dynamicProperty( "guidelines", """ An :ref:`type-immutable-list` of font-level :class:`BaseGuideline` objects. >>> for guideline in font.guidelines: ... guideline.angle 0 45 90 """ ) def _get_guidelines(self): """ This is the environment implementation of :attr:`BaseFont.guidelines`. This must return an :ref:`type-immutable-list` of :class:`BaseGuideline` objects. Subclasses may override this method. """ return tuple([self._getitem__guidelines(i) for i in range(self._len__guidelines())]) def _len__guidelines(self): return self._lenGuidelines() def _lenGuidelines(self, **kwargs): """ This must return an integer indicating the number of font-level guidelines in the font. Subclasses must override this method. """ self.raiseNotImplementedError() def _getitem__guidelines(self, index): index = normalizers.normalizeIndex(index) if index >= self._len__guidelines(): raise ValueError("No guideline located at index %d." % index) guideline = self._getGuideline(index) self._setFontInGuideline(guideline) return guideline def _getGuideline(self, index, **kwargs): """ This must return a :class:`BaseGuideline` object. **index** will be a valid **index**. Subclasses must override this method. """ self.raiseNotImplementedError() def _getGuidelineIndex(self, guideline): for i, other in enumerate(self.guidelines): if guideline == other: return i raise FontPartsError("The guideline could not be found.") def appendGuideline(self, position=None, angle=None, name=None, color=None, guideline=None): """ Append a new guideline to the font. >>> guideline = font.appendGuideline((50, 0), 90) >>> guideline = font.appendGuideline((0, 540), 0, name="overshoot", >>> color=(0, 0, 0, 0.2)) **position** must be a :ref:`type-coordinate` indicating the position of the guideline. **angle** indicates the :ref:`type-angle` of the guideline. **name** indicates the name for the guideline. This must be a :ref:`type-string` or ``None``. **color** indicates the color for the guideline. This must be a :ref:`type-color` or ``None``. This will return the newly created :class:`BaseGuidline` object. ``guideline`` may be a :class:`BaseGuideline` object from which attribute values will be copied. If ``position``, ``angle``, ``name`` or ``color`` are specified as arguments, those values will be used instead of the values in the given guideline object. """ identifier = None if guideline is not None: guideline = normalizers.normalizeGuideline(guideline) if position is None: position = guideline.position if angle is None: angle = guideline.angle if name is None: name = guideline.name if color is None: color = guideline.color if guideline.identifier is not None: existing = set([g.identifier for g in self.guidelines if g.identifier is not None]) if guideline.identifier not in existing: identifier = guideline.identifier position = normalizers.normalizeCoordinateTuple(position) angle = normalizers.normalizeRotationAngle(angle) if name is not None: name = normalizers.normalizeGuidelineName(name) if color is not None: color = normalizers.normalizeColor(color) identifier = normalizers.normalizeIdentifier(identifier) guideline = self._appendGuideline(position, angle, name=name, color=color, identifier=identifier) guideline.font = self return guideline def _appendGuideline(self, position, angle, name=None, color=None, identifier=None, **kwargs): """ This is the environment implementation of :meth:`BaseFont.appendGuideline`. **position** will be a valid :ref:`type-coordinate`. **angle** will be a valid angle. **name** will be a valid :ref:`type-string` or ``None``. **color** will be a valid :ref:`type-color` or ``None``. This must return the newly created :class:`BaseGuideline` object. Subclasses may override this method. """ self.raiseNotImplementedError() def removeGuideline(self, guideline): """ Remove **guideline** from the font. >>> font.removeGuideline(guideline) >>> font.removeGuideline(2) **guideline** can be a guideline object or an integer representing the guideline index. """ if isinstance(guideline, int): index = guideline else: index = self._getGuidelineIndex(guideline) index = normalizers.normalizeIndex(index) if index >= self._len__guidelines(): raise ValueError("No guideline located at index %d." % index) self._removeGuideline(index) def _removeGuideline(self, index, **kwargs): """ This is the environment implementation of :meth:`BaseFont.removeGuideline`. **index** will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def clearGuidelines(self): """ Clear all guidelines. >>> font.clearGuidelines() """ self._clearGuidelines() def _clearGuidelines(self): """ This is the environment implementation of :meth:`BaseFont.clearGuidelines`. Subclasses may override this method. """ for _ in range(self._len__guidelines()): self.removeGuideline(-1) # ------------- # Interpolation # ------------- def interpolate(self, factor, minFont, maxFont, round=True, suppressError=True): """ Interpolate all possible data in the font. >>> font.interpolate(0.5, otherFont1, otherFont2) >>> font.interpolate((0.5, 2.0), otherFont1, otherFont2, round=False) The interpolation occurs on a 0 to 1.0 range where **minFont** is located at 0 and **maxFont** is located at 1.0. **factor** is the interpolation value. It may be less than 0 and greater than 1.0. It may be a :ref:`type-int-float` or a tuple of two :ref:`type-int-float`. If it is a tuple, the first number indicates the x factor and the second number indicates the y factor. **round** indicates if the result should be rounded to integers. **suppressError** indicates if incompatible data should be ignored or if an error should be raised when such incompatibilities are found. """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minFont, BaseFont): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, minFont.__class__.__name__)) if not isinstance(maxFont, BaseFont): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, maxFont.__class__.__name__)) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate(factor, minFont, maxFont, round=round, suppressError=suppressError) def _interpolate(self, factor, minFont, maxFont, round=True, suppressError=True): """ This is the environment implementation of :meth:`BaseFont.interpolate`. Subclasses may override this method. """ # layers for layerName in self.layerOrder: self.removeLayer(layerName) for layerName in minFont.layerOrder: if layerName not in maxFont.layerOrder: continue minLayer = minFont.getLayer(layerName) maxLayer = maxFont.getLayer(layerName) dstLayer = self.newLayer(layerName) dstLayer.interpolate(factor, minLayer, maxLayer, round=round, suppressError=suppressError) if self.layerOrder: if ufoLib.DEFAULT_LAYER_NAME in self.layerOrder: self.defaultLayer = self.getLayer(ufoLib.DEFAULT_LAYER_NAME) else: self.defaultLayer = self.getLayer(self.layerOrder[0]) # kerning and groups self.kerning.interpolate(factor, minFont.kerning, maxFont.kerning, round=round, suppressError=suppressError) # info self.info.interpolate(factor, minFont.info, maxFont.info, round=round, suppressError=suppressError) compatibilityReporterClass = FontCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. >>> compatible, report = self.isCompatible(otherFont) >>> compatible False >>> report [Fatal] Glyph: "test1" + "test2" [Fatal] Glyph: "test1" contains 1 contours | "test2" contains 2 contours This will return a ``bool`` indicating if the font is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseFont, self).isCompatible(other, BaseFont) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseFont.isCompatible`. Subclasses may override this method. """ font1 = self font2 = other # incompatible guidelines guidelines1 = set(font1.guidelines) guidelines2 = set(font2.guidelines) if len(guidelines1) != len(guidelines2): reporter.warning = True reporter.guidelineCountDifference = True if len(guidelines1.difference(guidelines2)) != 0: reporter.warning = True reporter.guidelinesMissingFromFont2 = list( guidelines1.difference(guidelines2)) if len(guidelines2.difference(guidelines1)) != 0: reporter.warning = True reporter.guidelinesMissingInFont1 = list( guidelines2.difference(guidelines1)) # incompatible layers layers1 = set(font1.layerOrder) layers2 = set(font2.layerOrder) if len(layers1) != len(layers2): reporter.warning = True reporter.layerCountDifference = True if len(layers1.difference(layers2)) != 0: reporter.warning = True reporter.layersMissingFromFont2 = list(layers1.difference(layers2)) if len(layers2.difference(layers1)) != 0: reporter.warning = True reporter.layersMissingInFont1 = list(layers2.difference(layers1)) # test layers for layerName in sorted(layers1.intersection(layers2)): layer1 = font1.getLayer(layerName) layer2 = font2.getLayer(layerName) layerCompatibility = layer1.isCompatible(layer2)[1] if layerCompatibility.fatal or layerCompatibility.warning: if layerCompatibility.fatal: reporter.fatal = True if layerCompatibility.warning: reporter.warning = True reporter.layers.append(layerCompatibility) # ------- # mapping # ------- def getReverseComponentMapping(self): """ Get a reversed map of component references in the font. { 'A' : ['Aacute', 'Aring'] 'acute' : ['Aacute'] 'ring' : ['Aring'] etc. } """ return self._getReverseComponentMapping() def _getReverseComponentMapping(self): """ This is the environment implementation of :meth:`BaseFont.getReverseComponentMapping`. Subclasses may override this method. """ layer = self.defaultLayer return layer.getReverseComponentMapping() def getCharacterMapping(self): """ Create a dictionary of unicode -> [glyphname, ...] mappings. All glyphs are loaded. Note that one glyph can have multiple unicode values, and a unicode value can have multiple glyphs pointing to it. """ return self._getCharacterMapping() def _getCharacterMapping(self): """ This is the environment implementation of :meth:`BaseFont.getCharacterMapping`. Subclasses may override this method. """ layer = self.defaultLayer return layer.getCharacterMapping() # --------- # Selection # --------- # layers selectedLayers = dynamicProperty( "base_selectedLayers", """ A list of layers selected in the layer. Getting selected layer objects: >>> for layer in layer.selectedLayers: ... layer.color = (1, 0, 0, 0.5) Setting selected layer objects: >>> layer.selectedLayers = someLayers """ ) def _get_base_selectedLayers(self): selected = tuple([normalizers.normalizeLayer(layer) for layer in self._get_selectedLayers()]) return selected def _get_selectedLayers(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.layers) def _set_base_selectedLayers(self, value): normalized = [normalizers.normalizeLayer(layer) for layer in value] self._set_selectedLayers(normalized) def _set_selectedLayers(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.layers, value) selectedLayerNames = dynamicProperty( "base_selectedLayerNames", """ A list of names of layers selected in the layer. Getting selected layer names: >>> for name in layer.selectedLayerNames: ... print(name) Setting selected layer names: >>> layer.selectedLayerNames = ["A", "B", "C"] """ ) def _get_base_selectedLayerNames(self): selected = tuple([normalizers.normalizeLayerName(name) for name in self._get_selectedLayerNames()]) return selected def _get_selectedLayerNames(self): """ Subclasses may override this method. """ selected = [layer.name for layer in self.selectedLayers] return selected def _set_base_selectedLayerNames(self, value): normalized = [normalizers.normalizeLayerName(name) for name in value] self._set_selectedLayerNames(normalized) def _set_selectedLayerNames(self, value): """ Subclasses may override this method. """ select = [self.layers(name) for name in value] self.selectedLayers = select # guidelines selectedGuidelines = dynamicProperty( "base_selectedGuidelines", """ A list of guidelines selected in the font. Getting selected guideline objects: >>> for guideline in font.selectedGuidelines: ... guideline.color = (1, 0, 0, 0.5) Setting selected guideline objects: >>> font.selectedGuidelines = someGuidelines Setting also supports guideline indexes: >>> font.selectedGuidelines = [0, 2] """ ) def _get_base_selectedGuidelines(self): selected = tuple([normalizers.normalizeGuideline(guideline) for guideline in self._get_selectedGuidelines()]) return selected def _get_selectedGuidelines(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.guidelines) def _set_base_selectedGuidelines(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeIndex(i) else: i = normalizers.normalizeGuideline(i) normalized.append(i) self._set_selectedGuidelines(normalized) def _set_selectedGuidelines(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.guidelines, value) robotools-fontParts-26e8b8c/Lib/fontParts/base/glyph.py000066400000000000000000002354531477533125200232570ustar00rootroot00000000000000try: from itertools import zip_longest as zip_longest except ImportError: from itertools import izip_longest as zip_longest import collections import os from copy import deepcopy from fontParts.base.errors import FontPartsError from fontParts.base.base import ( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, dynamicProperty, interpolate, FuzzyNumber ) from fontParts.base import normalizers from fontParts.base.compatibility import GlyphCompatibilityReporter from fontParts.base.color import Color from fontParts.base.deprecated import DeprecatedGlyph, RemovedGlyph class BaseGlyph(BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, DeprecatedGlyph, RemovedGlyph ): """ A glyph object. This object will almost always be created by retrieving it from a font object. """ copyAttributes = ( "name", "unicodes", "width", "height", "note", "markColor", "lib" ) def _reprContents(self): contents = [ "'%s'" % self.name, ] if self.layer is not None: contents.append("('%s')" % self.layer.name) return contents def copy(self): """ Copy this glyph's data into a new glyph object. This new glyph object will not belong to a font. >>> copiedGlyph = glyph.copy() This will copy: - name - unicodes - width - height - note - markColor - lib - contours - components - anchors - guidelines - image """ return super(BaseGlyph, self).copy() def copyData(self, source): super(BaseGlyph, self).copyData(source) for contour in source.contours: self.appendContour(contour) for component in source.components: self.appendComponent(component=component) for anchor in source.anchors: self.appendAnchor(anchor=anchor) for guideline in source.guidelines: self.appendGuideline(guideline=guideline) sourceImage = source.image if sourceImage.data is not None: selfImage = self.addImage(data=sourceImage.data) selfImage.transformation = sourceImage.transformation selfImage.color = sourceImage.color # ------- # Parents # ------- # Layer _layer = None layer = dynamicProperty( "layer", """ The glyph's parent layer. >>> layer = glyph.layer """ ) def _get_layer(self): if self._layer is None: return None return self._layer def _set_layer(self, layer): self._layer = layer # Font font = dynamicProperty( "font", """ The glyph's parent font. >>> font = glyph.font """ ) def _get_font(self): if self._layer is None: return None return self.layer.font # -------------- # Identification # -------------- # Name name = dynamicProperty( "base_name", """ The glyph's name. This will be a :ref:`type-string`. >>> glyph.name "A" >>> glyph.name = "A.alt" """ ) def _get_base_name(self): value = self._get_name() if value is not None: value = normalizers.normalizeGlyphName(value) return value def _set_base_name(self, value): if value == self.name: return value = normalizers.normalizeGlyphName(value) layer = self.layer if layer is not None and value in layer: raise ValueError("A glyph with the name '%s' already exists." % value) self._set_name(value) def _get_name(self): """ Get the name of the glyph. This must return a unicode string. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value): """ Set the name of the glyph. This will be a unicode string. Subclasses must override this method. """ self.raiseNotImplementedError() # Unicodes unicodes = dynamicProperty( "base_unicodes", """ The glyph's unicode values in order from most to least important. >>> glyph.unicodes (65,) >>> glyph.unicodes = [65, 66] >>> glyph.unicodes = [] The values in the returned tuple will be :ref:`type-int`. When setting you may use a list of :ref:`type-int` or :ref:`type-hex` values. """ ) def _get_base_unicodes(self): value = self._get_unicodes() value = normalizers.normalizeGlyphUnicodes(value) return value def _set_base_unicodes(self, value): value = list(value) value = normalizers.normalizeGlyphUnicodes(value) self._set_unicodes(value) def _get_unicodes(self): """ Get the unicodes assigned to the glyph. This must return a tuple of zero or more integers. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_unicodes(self, value): """ Assign the unicodes to the glyph. This will be a list of zero or more integers. Subclasses must override this method. """ self.raiseNotImplementedError() unicode = dynamicProperty( "base_unicode", """ The glyph's primary unicode value. >>> glyph.unicode 65 >>> glyph.unicode = None This is equivalent to ``glyph.unicodes[0]``. Setting a ``glyph.unicode`` value will reset ``glyph.unicodes`` to a tuple containing that value or an empty tuple if ``value`` is ``None``. >>> glyph.unicodes (65, 67) >>> glyph.unicode = 65 >>> glyph.unicodes (65,) >>> glyph.unicode = None >>> glyph.unicodes () The returned value will be an :ref:`type-int` or ``None``. When setting you may send :ref:`type-int` or :ref:`type-hex` values or ``None``. """ ) def _get_base_unicode(self): value = self._get_unicode() if value is not None: value = normalizers.normalizeGlyphUnicode(value) return value def _set_base_unicode(self, value): if value is not None: value = normalizers.normalizeGlyphUnicode(value) self._set_unicode(value) else: self._set_unicodes(()) def _get_unicode(self): """ Get the primary unicode assigned to the glyph. This must return an integer or None. Subclasses may override this method. """ values = self.unicodes if values: return values[0] return None def _set_unicode(self, value): """ Assign the primary unicode to the glyph. This will be an integer or None. Subclasses may override this method. """ if value is None: self.unicodes = [] else: self.unicodes = [value] def autoUnicodes(self): """ Use heuristics to set the Unicode values in the glyph. >>> glyph.autoUnicodes() Environments will define their own heuristics for automatically determining values. """ self._autoUnicodes() def _autoUnicodes(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() # ------- # Metrics # ------- # horizontal width = dynamicProperty( "base_width", """ The glyph's width. >>> glyph.width 500 >>> glyph.width = 200 The value will be a :ref:`type-int-float`. """ ) def _get_base_width(self): value = self._get_width() value = normalizers.normalizeGlyphWidth(value) return value def _set_base_width(self, value): value = normalizers.normalizeGlyphWidth(value) self._set_width(value) def _get_width(self): """ This must return an int or float. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_width(self, value): """ value will be an int or float. Subclasses must override this method. """ self.raiseNotImplementedError() leftMargin = dynamicProperty( "base_leftMargin", """ The glyph's left margin. >>> glyph.leftMargin 35 >>> glyph.leftMargin = 45 The value will be a :ref:`type-int-float` or `None` if the glyph has no outlines. """ ) def _get_base_leftMargin(self): value = self._get_leftMargin() value = normalizers.normalizeGlyphLeftMargin(value) return value def _set_base_leftMargin(self, value): value = normalizers.normalizeGlyphLeftMargin(value) self._set_leftMargin(value) def _get_leftMargin(self): """ This must return an int or float. If the glyph has no outlines, this must return `None`. Subclasses may override this method. """ bounds = self.bounds if bounds is None: return None xMin, yMin, xMax, yMax = bounds return xMin def _set_leftMargin(self, value): """ value will be an int or float. Subclasses may override this method. """ diff = value - self.leftMargin self.moveBy((diff, 0)) self.width += diff rightMargin = dynamicProperty( "base_rightMargin", """ The glyph's right margin. >>> glyph.rightMargin 35 >>> glyph.rightMargin = 45 The value will be a :ref:`type-int-float` or `None` if the glyph has no outlines. """ ) def _get_base_rightMargin(self): value = self._get_rightMargin() value = normalizers.normalizeGlyphRightMargin(value) return value def _set_base_rightMargin(self, value): value = normalizers.normalizeGlyphRightMargin(value) self._set_rightMargin(value) def _get_rightMargin(self): """ This must return an int or float. If the glyph has no outlines, this must return `None`. Subclasses may override this method. """ bounds = self.bounds if bounds is None: return None xMin, yMin, xMax, yMax = bounds return self.width - xMax def _set_rightMargin(self, value): """ value will be an int or float. Subclasses may override this method. """ bounds = self.bounds if bounds is None: self.width = value else: xMin, yMin, xMax, yMax = bounds self.width = xMax + value # vertical height = dynamicProperty( "base_height", """ The glyph's height. >>> glyph.height 500 >>> glyph.height = 200 The value will be a :ref:`type-int-float`. """ ) def _get_base_height(self): value = self._get_height() value = normalizers.normalizeGlyphHeight(value) return value def _set_base_height(self, value): value = normalizers.normalizeGlyphHeight(value) self._set_height(value) def _get_height(self): """ This must return an int or float. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_height(self, value): """ value will be an int or float. Subclasses must override this method. """ self.raiseNotImplementedError() bottomMargin = dynamicProperty( "base_bottomMargin", """ The glyph's bottom margin. >>> glyph.bottomMargin 35 >>> glyph.bottomMargin = 45 The value will be a :ref:`type-int-float` or `None` if the glyph has no outlines. """ ) def _get_base_bottomMargin(self): value = self._get_bottomMargin() value = normalizers.normalizeGlyphBottomMargin(value) return value def _set_base_bottomMargin(self, value): value = normalizers.normalizeGlyphBottomMargin(value) self._set_bottomMargin(value) def _get_bottomMargin(self): """ This must return an int or float. If the glyph has no outlines, this must return `None`. Subclasses may override this method. """ bounds = self.bounds if bounds is None: return None xMin, yMin, xMax, yMax = bounds return yMin def _set_bottomMargin(self, value): """ value will be an int or float. Subclasses may override this method. """ diff = value - self.bottomMargin self.moveBy((0, diff)) self.height += diff topMargin = dynamicProperty( "base_topMargin", """ The glyph's top margin. >>> glyph.topMargin 35 >>> glyph.topMargin = 45 The value will be a :ref:`type-int-float` or `None` if the glyph has no outlines. """ ) def _get_base_topMargin(self): value = self._get_topMargin() value = normalizers.normalizeGlyphTopMargin(value) return value def _set_base_topMargin(self, value): value = normalizers.normalizeGlyphTopMargin(value) self._set_topMargin(value) def _get_topMargin(self): """ This must return an int or float. If the glyph has no outlines, this must return `None`. Subclasses may override this method. """ bounds = self.bounds if bounds is None: return None xMin, yMin, xMax, yMax = bounds return self.height - yMax def _set_topMargin(self, value): """ value will be an int or float. Subclasses may override this method. """ bounds = self.bounds if bounds is None: self.height = value else: xMin, yMin, xMax, yMax = bounds self.height = yMax + value # ---- # Pens # ---- def getPen(self): """ Return a :ref:`type-pen` object for adding outline data to the glyph. >>> pen = glyph.getPen() """ self.raiseNotImplementedError() def getPointPen(self): """ Return a :ref:`type-pointpen` object for adding outline data to the glyph. >>> pointPen = glyph.getPointPen() """ self.raiseNotImplementedError() def draw(self, pen, contours=True, components=True): """ Draw the glyph's outline data (contours and components) to the given :ref:`type-pen`. >>> glyph.draw(pen) If ``contours`` is set to ``False``, the glyph's contours will not be drawn. >>> glyph.draw(pen, contours=False) If ``components`` is set to ``False``, the glyph's components will not be drawn. >>> glyph.draw(pen, components=False) """ if contours: for contour in self: contour.draw(pen) if components: for component in self.components: component.draw(pen) def drawPoints(self, pen, contours=True, components=True): """ Draw the glyph's outline data (contours and components) to the given :ref:`type-pointpen`. >>> glyph.drawPoints(pointPen) If ``contours`` is set to ``False``, the glyph's contours will not be drawn. >>> glyph.drawPoints(pointPen, contours=False) If ``components`` is set to ``False``, the glyph's components will not be drawn. >>> glyph.drawPoints(pointPen, components=False) """ if contours: for contour in self: contour.drawPoints(pen) if components: for component in self.components: component.drawPoints(pen) # ----------------------------------------- # Contour, Component and Anchor Interaction # ----------------------------------------- def clear(self, contours=True, components=True, anchors=True, guidelines=True, image=True): """ Clear the glyph. >>> glyph.clear() This clears: - contours - components - anchors - guidelines - image It's possible to turn off the clearing of portions of the glyph with the listed arguments. >>> glyph.clear(guidelines=False) """ self._clear(contours=contours, components=components, anchors=anchors, guidelines=guidelines, image=image) def _clear(self, contours=True, components=True, anchors=True, guidelines=True, image=True): """ Subclasses may override this method. """ if contours: self.clearContours() if components: self.clearComponents() if anchors: self.clearAnchors() if guidelines: self.clearGuidelines() if image: self.clearImage() def appendGlyph(self, other, offset=None): """ Append the data from ``other`` to new objects in this glyph. >>> glyph.appendGlyph(otherGlyph) This will append: - contours - components - anchors - guidelines ``offset`` indicates the x and y shift values that should be applied to the appended data. It must be a :ref:`type-coordinate` value or ``None``. If ``None`` is given, the offset will be ``(0, 0)``. >>> glyph.appendGlyph(otherGlyph, (100, 0)) """ if offset is None: offset = (0, 0) offset = normalizers.normalizeTransformationOffset(offset) self._appendGlyph(other, offset) def _appendGlyph(self, other, offset=None): """ Subclasses may override this method. """ other = other.copy() if offset != (0, 0): other.moveBy(offset) for contour in other.contours: self.appendContour(contour) for component in other.components: self.appendComponent(component=component) for anchor in other.anchors: self.appendAnchor(anchor=anchor) for guideline in other.guidelines: self.appendGuideline(guideline=guideline) # Contours def _setGlyphInContour(self, contour): if contour.glyph is None: contour.glyph = self contours = dynamicProperty( "contours", """ An :ref:`type-immutable-list` of all contours in the glyph. >>> contours = glyph.contours The list will contain :class:`BaseContour` objects. """ ) def _get_contours(self): """ Subclasses may override this method. """ return tuple([self[i] for i in range(len(self))]) def __len__(self): """ The number of contours in the glyph. >>> len(glyph) 2 """ return self._lenContours() def _lenContours(self, **kwargs): """ This must return an integer. Subclasses must override this method. """ self.raiseNotImplementedError() def __iter__(self): """ Iterate through all contours in the glyph. >>> for contour in glyph: ... contour.reverse() """ return self._iterContours() def _iterContours(self, **kwargs): """ This must return an iterator that returns wrapped contours. Subclasses may override this method. """ count = len(self) index = 0 while count: yield self[index] count -= 1 index += 1 def __getitem__(self, index): """ Get the contour located at ``index`` from the glyph. >>> contour = glyph[0] The returned value will be a :class:`BaseContour` object. """ index = normalizers.normalizeIndex(index) if index >= len(self): raise ValueError("No contour located at index %d." % index) contour = self._getContour(index) self._setGlyphInContour(contour) return contour def _getContour(self, index, **kwargs): """ This must return a wrapped contour. index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def _getContourIndex(self, contour): for i, other in enumerate(self.contours): if contour == other: return i raise FontPartsError("The contour could not be found.") def appendContour(self, contour, offset=None): """ Append a contour containing the same data as ``contour`` to this glyph. >>> contour = glyph.appendContour(contour) This will return a :class:`BaseContour` object representing the new contour in the glyph. ``offset`` indicates the x and y shift values that should be applied to the appended data. It must be a :ref:`type-coordinate` value or ``None``. If ``None`` is given, the offset will be ``(0, 0)``. >>> contour = glyph.appendContour(contour, (100, 0)) """ contour = normalizers.normalizeContour(contour) if offset is None: offset = (0, 0) offset = normalizers.normalizeTransformationOffset(offset) return self._appendContour(contour, offset) def _appendContour(self, contour, offset=None, **kwargs): """ contour will be an object with a drawPoints method. offset will be a valid offset (x, y). This must return the new contour. Subclasses may override this method. """ pointPen = self.getPointPen() if offset != (0, 0): copy = contour.copy() copy.moveBy(offset) copy.drawPoints(pointPen) else: contour.drawPoints(pointPen) return self[-1] def removeContour(self, contour): """ Remove ``contour`` from the glyph. >>> glyph.removeContour(contour) ``contour`` may be a :ref:`BaseContour` or an :ref:`type-int` representing a contour index. """ if isinstance(contour, int): index = contour else: index = self._getContourIndex(contour) index = normalizers.normalizeIndex(index) if index >= len(self): raise ValueError("No contour located at index %d." % index) self._removeContour(index) def _removeContour(self, index, **kwargs): """ index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def clearContours(self): """ Clear all contours in the glyph. >>> glyph.clearContours() """ self._clearContours() def _clearContours(self): """ Subclasses may override this method. """ for _ in range(len(self)): self.removeContour(-1) def removeOverlap(self): """ Perform a remove overlap operation on the contours. >>> glyph.removeOverlap() The behavior of this may vary across environments. """ self._removeOverlap() def _removeOverlap(self): """ Subclasses must implement this method. """ self.raiseNotImplementedError() # Components def _setGlyphInComponent(self, component): if component.glyph is None: component.glyph = self components = dynamicProperty( "components", """ An :ref:`type-immutable-list` of all components in the glyph. >>> components = glyph.components The list will contain :class:`BaseComponent` objects. """ ) def _get_components(self): """ Subclasses may override this method. """ return tuple([self._getitem__components(i) for i in range(self._len__components())]) def _len__components(self): return self._lenComponents() def _lenComponents(self, **kwargs): """ This must return an integer indicating the number of components in the glyph. Subclasses must override this method. """ self.raiseNotImplementedError() def _getitem__components(self, index): index = normalizers.normalizeIndex(index) if index >= self._len__components(): raise ValueError("No component located at index %d." % index) component = self._getComponent(index) self._setGlyphInComponent(component) return component def _getComponent(self, index, **kwargs): """ This must return a wrapped component. index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def _getComponentIndex(self, component): for i, other in enumerate(self.components): if component == other: return i raise FontPartsError("The component could not be found.") def appendComponent(self, baseGlyph=None, offset=None, scale=None, component=None): """ Append a component to this glyph. >>> component = glyph.appendComponent("A") This will return a :class:`BaseComponent` object representing the new component in the glyph. ``offset`` indicates the x and y shift values that should be applied to the appended component. It must be a :ref:`type-coordinate` value or ``None``. If ``None`` is given, the offset will be ``(0, 0)``. >>> component = glyph.appendComponent("A", offset=(10, 20)) ``scale`` indicates the x and y scale values that should be applied to the appended component. It must be a :ref:`type-scale` value or ``None``. If ``None`` is given, the scale will be ``(1.0, 1.0)``. >>> component = glyph.appendComponent("A", scale=(1.0, 2.0)) ``component`` may be a :class:`BaseComponent` object from which attribute values will be copied. If ``baseGlyph``, ``offset`` or ``scale`` are specified as arguments, those values will be used instead of the values in the given component object. """ identifier = None sxy = 0 syx = 0 if component is not None: component = normalizers.normalizeComponent(component) if baseGlyph is None: baseGlyph = component.baseGlyph sx, sxy, syx, sy, ox, oy = component.transformation if offset is None: offset = (ox, oy) if scale is None: scale = (sx, sy) if baseGlyph is None: baseGlyph = component.baseGlyph if component.identifier is not None: existing = set([c.identifier for c in self.components if c.identifier is not None]) if component.identifier not in existing: identifier = component.identifier baseGlyph = normalizers.normalizeGlyphName(baseGlyph) if self.name == baseGlyph: raise FontPartsError(("A glyph cannot contain a component referencing itself.")) if offset is None: offset = (0, 0) if scale is None: scale = (1, 1) offset = normalizers.normalizeTransformationOffset(offset) scale = normalizers.normalizeTransformationScale(scale) ox, oy = offset sx, sy = scale transformation = (sx, sxy, syx, sy, ox, oy) identifier = normalizers.normalizeIdentifier(identifier) return self._appendComponent(baseGlyph, transformation=transformation, identifier=identifier) def _appendComponent(self, baseGlyph, transformation=None, identifier=None, **kwargs): """ baseGlyph will be a valid glyph name. The baseGlyph may or may not be in the layer. offset will be a valid offset (x, y). scale will be a valid scale (x, y). identifier will be a valid, nonconflicting identifier. This must return the new component. Subclasses may override this method. """ pointPen = self.getPointPen() pointPen.addComponent(baseGlyph, transformation=transformation, identifier=identifier) return self.components[-1] def removeComponent(self, component): """ Remove ``component`` from the glyph. >>> glyph.removeComponent(component) ``component`` may be a :ref:`BaseComponent` or an :ref:`type-int` representing a component index. """ if isinstance(component, int): index = component else: index = self._getComponentIndex(component) index = normalizers.normalizeIndex(index) if index >= self._len__components(): raise ValueError("No component located at index %d." % index) self._removeComponent(index) def _removeComponent(self, index, **kwargs): """ index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def clearComponents(self): """ Clear all components in the glyph. >>> glyph.clearComponents() """ self._clearComponents() def _clearComponents(self): """ Subclasses may override this method. """ for _ in range(self._len__components()): self.removeComponent(-1) def decompose(self): """ Decompose all components in the glyph to contours. >>> glyph.decompose() """ self._decompose() def _decompose(self): """ Subclasses may override this method. """ for component in self.components: component.decompose() # Anchors def _setGlyphInAnchor(self, anchor): if anchor.glyph is None: anchor.glyph = self anchors = dynamicProperty( "anchors", """ An :ref:`type-immutable-list` of all anchors in the glyph. >>> anchors = glyph.anchors The list will contain :class:`BaseAnchor` objects. """ ) def _get_anchors(self): """ Subclasses may override this method. """ return tuple([self._getitem__anchors(i) for i in range(self._len__anchors())]) def _len__anchors(self): return self._lenAnchors() def _lenAnchors(self, **kwargs): """ This must return an integer indicating the number of anchors in the glyph. Subclasses must override this method. """ self.raiseNotImplementedError() def _getitem__anchors(self, index): index = normalizers.normalizeIndex(index) if index >= self._len__anchors(): raise ValueError("No anchor located at index %d." % index) anchor = self._getAnchor(index) self._setGlyphInAnchor(anchor) return anchor def _getAnchor(self, index, **kwargs): """ This must return a wrapped anchor. index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def _getAnchorIndex(self, anchor): for i, other in enumerate(self.anchors): if anchor == other: return i raise FontPartsError("The anchor could not be found.") def appendAnchor(self, name=None, position=None, color=None, anchor=None): """ Append an anchor to this glyph. >>> anchor = glyph.appendAnchor("top", (10, 20)) This will return a :class:`BaseAnchor` object representing the new anchor in the glyph. ``name`` indicated the name to be assigned to the anchor. It must be a :ref:`type-string` or ``None``. ``position`` indicates the x and y location to be applied to the anchor. It must be a :ref:`type-coordinate` value. ``color`` indicates the color to be applied to the anchor. It must be a :ref:`type-color` or ``None``. >>> anchor = glyph.appendAnchor("top", (10, 20), color=(1, 0, 0, 1)) ``anchor`` may be a :class:`BaseAnchor` object from which attribute values will be copied. If ``name``, ``position`` or ``color`` are specified as arguments, those values will be used instead of the values in the given anchor object. """ identifier = None if anchor is not None: anchor = normalizers.normalizeAnchor(anchor) if name is None: name = anchor.name if position is None: position = anchor.position if color is None: color = anchor.color if anchor.identifier is not None: existing = set([a.identifier for a in self.anchors if a.identifier is not None]) if anchor.identifier not in existing: identifier = anchor.identifier name = normalizers.normalizeAnchorName(name) position = normalizers.normalizeCoordinateTuple(position) if color is not None: color = normalizers.normalizeColor(color) identifier = normalizers.normalizeIdentifier(identifier) return self._appendAnchor(name, position=position, color=color, identifier=identifier) def _appendAnchor(self, name, position=None, color=None, identifier=None, **kwargs): """ name will be a valid anchor name. position will be a valid position (x, y). color will be None or a valid color. identifier will be a valid, nonconflicting identifier. This must return the new anchor. Subclasses may override this method. """ self.raiseNotImplementedError() def removeAnchor(self, anchor): """ Remove ``anchor`` from the glyph. >>> glyph.removeAnchor(anchor) ``anchor`` may be an :ref:`BaseAnchor` or an :ref:`type-int` representing an anchor index. """ if isinstance(anchor, int): index = anchor else: index = self._getAnchorIndex(anchor) index = normalizers.normalizeIndex(index) if index >= self._len__anchors(): raise ValueError("No anchor located at index %d." % index) self._removeAnchor(index) def _removeAnchor(self, index, **kwargs): """ index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def clearAnchors(self): """ Clear all anchors in the glyph. >>> glyph.clearAnchors() """ self._clearAnchors() def _clearAnchors(self): """ Subclasses may override this method. """ for _ in range(self._len__anchors()): self.removeAnchor(-1) # ---------- # Guidelines # ---------- def _setGlyphInGuideline(self, guideline): if guideline.glyph is None: guideline.glyph = self guidelines = dynamicProperty( "guidelines", """ An :ref:`type-immutable-list` of all guidelines in the glyph. >>> guidelines = glyph.guidelines The list will contain :class:`BaseGuideline` objects. """ ) def _get_guidelines(self): """ Subclasses may override this method. """ return tuple([self._getitem__guidelines(i) for i in range(self._len__guidelines())]) def _len__guidelines(self): return self._lenGuidelines() def _lenGuidelines(self, **kwargs): """ This must return an integer indicating the number of guidelines in the glyph. Subclasses must override this method. """ self.raiseNotImplementedError() def _getitem__guidelines(self, index): index = normalizers.normalizeIndex(index) if index >= self._len__guidelines(): raise ValueError("No guideline located at index %d." % index) guideline = self._getGuideline(index) self._setGlyphInGuideline(guideline) return guideline def _getGuideline(self, index, **kwargs): """ This must return a wrapped guideline. index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def _getGuidelineIndex(self, guideline): for i, other in enumerate(self.guidelines): if guideline == other: return i raise FontPartsError("The guideline could not be found.") def appendGuideline(self, position=None, angle=None, name=None, color=None, guideline=None): """ Append a guideline to this glyph. >>> guideline = glyph.appendGuideline((100, 0), 90) This will return a :class:`BaseGuideline` object representing the new guideline in the glyph. ``position`` indicates the x and y location to be used as the center point of the anchor. It must be a :ref:`type-coordinate` value. ``angle`` indicates the angle of the guideline, in degrees. This must be a :ref:`type-int-float` between 0 and 360. ``name`` indicates an name to be assigned to the guideline. It must be a :ref:`type-string` or ``None``. >>> guideline = glyph.appendGuideline((100, 0), 90, name="left") ``color`` indicates the color to be applied to the guideline. It must be a :ref:`type-color` or ``None``. >>> guideline = glyph.appendGuideline((100, 0), 90, color=(1, 0, 0, 1)) ``guideline`` may be a :class:`BaseGuideline` object from which attribute values will be copied. If ``position``, ``angle``, ``name`` or ``color`` are specified as arguments, those values will be used instead of the values in the given guideline object. """ identifier = None if guideline is not None: guideline = normalizers.normalizeGuideline(guideline) if position is None: position = guideline.position if angle is None: angle = guideline.angle if name is None: name = guideline.name if color is None: color = guideline.color if guideline.identifier is not None: existing = set([g.identifier for g in self.guidelines if g.identifier is not None]) if guideline.identifier not in existing: identifier = guideline.identifier position = normalizers.normalizeCoordinateTuple(position) angle = normalizers.normalizeRotationAngle(angle) if name is not None: name = normalizers.normalizeGuidelineName(name) if color is not None: color = normalizers.normalizeColor(color) identifier = normalizers.normalizeIdentifier(identifier) guideline = self._appendGuideline(position, angle, name=name, color=color, identifier=identifier) guideline.glyph = self return guideline def _appendGuideline(self, position, angle, name=None, color=None, identifier=None, **kwargs): """ position will be a valid position (x, y). angle will be a valid angle. name will be a valid guideline name or None. color will be a valid color or None . identifier will be a valid, nonconflicting identifier. This must return the new guideline. Subclasses may override this method. """ self.raiseNotImplementedError() def removeGuideline(self, guideline): """ Remove ``guideline`` from the glyph. >>> glyph.removeGuideline(guideline) ``guideline`` may be a :ref:`BaseGuideline` or an :ref:`type-int` representing an guideline index. """ if isinstance(guideline, int): index = guideline else: index = self._getGuidelineIndex(guideline) index = normalizers.normalizeIndex(index) if index >= self._len__guidelines(): raise ValueError("No guideline located at index %d." % index) self._removeGuideline(index) def _removeGuideline(self, index, **kwargs): """ index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def clearGuidelines(self): """ Clear all guidelines in the glyph. >>> glyph.clearGuidelines() """ self._clearGuidelines() def _clearGuidelines(self): """ Subclasses may override this method. """ for _ in range(self._len__guidelines()): self.removeGuideline(-1) # ------------------ # Data Normalization # ------------------ def round(self): """ Round coordinates to the nearest integer. >>> glyph.round() This applies to the following: - width - height - contours - components - anchors - guidelines """ self._round() def _round(self): """ Subclasses may override this method. """ for contour in self.contours: contour.round() for component in self.components: component.round() for anchor in self.anchors: anchor.round() for guideline in self.guidelines: guideline.round() self.width = normalizers.normalizeVisualRounding(self.width) self.height = normalizers.normalizeVisualRounding(self.height) def correctDirection(self, trueType=False): """ Correct the winding direction of the contours following the PostScript recommendations. >>> glyph.correctDirection() If ``trueType`` is ``True`` the TrueType recommendations will be followed. """ self._correctDirection(trueType=trueType) def _correctDirection(self, trueType=False, **kwargs): """ Subclasses may override this method. """ self.raiseNotImplementedError() def autoContourOrder(self): """ Automatically order the contours based on heuristics. >>> glyph.autoContourOrder() The results of this may vary across environments. """ self._autoContourOrder() def _autoContourOrder(self, **kwargs): """ Sorting is based on (in this order): - the (negative) point count - the (negative) segment count - x value of the center of the contour rounded to a threshold - y value of the center of the contour rounded to a threshold (such threshold is calculated as the smallest contour width or height in the glyph divided by two) - the (negative) surface of the bounding box of the contour: width * height the latter is a safety net for for instances like a very thin 'O' where the x centers could be close enough to rely on the y for the sort which could very well be the same for both contours. We use the _negative_ of the surface to ensure that larger contours appear first, which seems more natural. """ tempContourList = [] contourList = [] xThreshold = None yThreshold = None for contour in self: bounds = contour.bounds if bounds is None: continue xMin, yMin, xMax, yMax = bounds width = xMax - xMin height = yMax - yMin xC = 0.5 * (xMin + xMax) yC = 0.5 * (yMin + yMax) xTh = abs(width * 0.5) yTh = abs(height * 0.5) if xThreshold is None or xThreshold > xTh: xThreshold = xTh if yThreshold is None or yThreshold > yTh: yThreshold = yTh tempContourList.append((-len(contour.points), -len(contour.segments), xC, yC, -(width * height), contour)) for points, segments, x, y, surface, contour in tempContourList: contourList.append((points, segments, FuzzyNumber(x, xThreshold), FuzzyNumber(y, yThreshold), surface, contour)) contourList.sort() self.clearContours() for points, segments, xO, yO, surface, contour in contourList: self.appendContour(contour) # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ for contour in self.contours: contour.transformBy(matrix) for component in self.components: component.transformBy(matrix) for anchor in self.anchors: anchor.transformBy(matrix) for guideline in self.guidelines: guideline.transformBy(matrix) def scaleBy(self, value, origin=None, width=False, height=False): """ %s **width** indicates if the glyph's width should be scaled. **height** indicates if the glyph's height should be scaled. The origin must not be specified when scaling the width or height. """ value = normalizers.normalizeTransformationScale(value) if origin is None: origin = (0, 0) origin = normalizers.normalizeCoordinateTuple(origin) if origin != (0, 0) and (width or height): raise FontPartsError(("The origin must not be set when " "scaling the width or height.")) super(BaseGlyph, self).scaleBy(value, origin=origin) sX, sY = value if width: self._scaleWidthBy(sX) if height: self._scaleHeightBy(sY) scaleBy.__doc__ %= TransformationMixin.scaleBy.__doc__ def _scaleWidthBy(self, value): """ Subclasses may override this method. """ self.width *= value def _scaleHeightBy(self, value): """ Subclasses may override this method. """ self.height *= value # -------------------- # Interpolation & Math # -------------------- def toMathGlyph(self, scaleComponentTransform=True, strict=False): """ Returns the glyph as an object that follows the `MathGlyph protocol `_. >>> mg = glyph.toMathGlyph() **scaleComponentTransform** Enables the MathGlyph `scaleComponentTransform` option. **strict** Enables the MathGlyph `strict` option. """ return self._toMathGlyph(scaleComponentTransform=scaleComponentTransform, strict=strict) def _toMathGlyph(self, scaleComponentTransform=True, strict=False): """ Subclasses may override this method. """ import fontMath mathGlyph = fontMath.MathGlyph( None, scaleComponentTransform=scaleComponentTransform, strict=strict ) pen = mathGlyph.getPointPen() self.drawPoints(pen) for anchor in self.anchors: d = dict( x=anchor.x, y=anchor.y, name=anchor.name, identifier=anchor.identifier, color=anchor.color ) mathGlyph.anchors.append(d) for guideline in self.guidelines: d = dict( x=guideline.x, y=guideline.y, angle=guideline.angle, name=guideline.name, identifier=guideline.identifier, color=guideline.color ) mathGlyph.guidelines.append(d) mathGlyph.lib = deepcopy(self.lib) mathGlyph.name = self.name mathGlyph.unicodes = self.unicodes mathGlyph.width = self.width mathGlyph.height = self.height mathGlyph.note = self.note return mathGlyph def fromMathGlyph(self, mathGlyph, filterRedundantPoints=True): """ Replaces the contents of this glyph with the contents of ``mathGlyph``. >>> glyph.fromMathGlyph(mg) ``mathGlyph`` must be an object following the `MathGlyph protocol `_. **filterRedundantPoints** enables the MathGlyph `drawPoints` `filterRedundantPoints` option. """ return self._fromMathGlyph(mathGlyph, toThisGlyph=True, filterRedundantPoints=filterRedundantPoints) def _fromMathGlyph(self, mathGlyph, toThisGlyph=False, filterRedundantPoints=True): # make the destination if toThisGlyph: copied = self copied.clear() else: copyClass = self.copyClass if copyClass is None: copyClass = self.__class__ copied = copyClass() # populate pen = copied.getPointPen() mathGlyph.drawPoints(pen, filterRedundantPoints=filterRedundantPoints) for anchor in mathGlyph.anchors: a = copied.appendAnchor( name=anchor.get("name"), position=(anchor["x"], anchor["y"]), color=anchor["color"] ) identifier = anchor.get("identifier") if identifier is not None: a._setIdentifier(identifier) for guideline in mathGlyph.guidelines: g = copied.appendGuideline( position=(guideline["x"], guideline["y"]), angle=guideline["angle"], name=guideline["name"], color=guideline["color"] ) identifier = guideline.get("identifier") if identifier is not None: g._setIdentifier(identifier) copied.lib.update(mathGlyph.lib) if not toThisGlyph: copied.name = mathGlyph.name copied.unicodes = mathGlyph.unicodes copied.width = mathGlyph.width copied.height = mathGlyph.height copied.note = mathGlyph.note return copied def __mul__(self, factor): """ Subclasses may override this method. """ mathGlyph = self._toMathGlyph() result = mathGlyph * factor copied = self._fromMathGlyph(result) return copied __rmul__ = __mul__ def __truediv__(self, factor): """ Subclasses may override this method. """ mathGlyph = self._toMathGlyph() result = mathGlyph / factor copied = self._fromMathGlyph(result) return copied # py2 support __div__ = __truediv__ def __add__(self, other): """ Subclasses may override this method. """ selfMathGlyph = self._toMathGlyph() otherMathGlyph = other._toMathGlyph() result = selfMathGlyph + otherMathGlyph copied = self._fromMathGlyph(result) return copied def __sub__(self, other): """ Subclasses may override this method. """ selfMathGlyph = self._toMathGlyph() otherMathGlyph = other._toMathGlyph() result = selfMathGlyph - otherMathGlyph copied = self._fromMathGlyph(result) return copied def interpolate(self, factor, minGlyph, maxGlyph, round=True, suppressError=True): """ Interpolate the contents of this glyph at location ``factor`` in a linear interpolation between ``minGlyph`` and ``maxGlyph``. >>> glyph.interpolate(0.5, otherGlyph1, otherGlyph2) ``factor`` may be a :ref:`type-int-float` or a tuple containing two :ref:`type-int-float` values representing x and y factors. >>> glyph.interpolate((0.5, 1.0), otherGlyph1, otherGlyph2) ``minGlyph`` must be a :class:`BaseGlyph` and will be located at 0.0 in the interpolation range. ``maxGlyph`` must be a :class:`BaseGlyph` and will be located at 1.0 in the interpolation range. If ``round`` is ``True``, the contents of the glyph will be rounded to integers after the interpolation is performed. >>> glyph.interpolate(0.5, otherGlyph1, otherGlyph2, round=True) This method assumes that ``minGlyph`` and ``maxGlyph`` are completely compatible with each other for interpolation. If not, any errors encountered will raise a :class:`FontPartsError`. If ``suppressError`` is ``True``, no exception will be raised and errors will be silently ignored. """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minGlyph, BaseGlyph): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, minGlyph.__class__.__name__)) if not isinstance(maxGlyph, BaseGlyph): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, maxGlyph.__class__.__name__)) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate(factor, minGlyph, maxGlyph, round=round, suppressError=suppressError) def _interpolate(self, factor, minGlyph, maxGlyph, round=True, suppressError=True): """ Subclasses may override this method. """ from fontMath.mathFunctions import setRoundIntegerFunction setRoundIntegerFunction(normalizers.normalizeVisualRounding) minGlyph = minGlyph._toMathGlyph() maxGlyph = maxGlyph._toMathGlyph() try: result = interpolate(minGlyph, maxGlyph, factor) except IndexError: result = None if result is None and not suppressError: raise FontPartsError(("Glyphs '%s' and '%s' could not be " "interpolated.") % (minGlyph.name, maxGlyph.name)) if result is not None: if round: result = result.round() self._fromMathGlyph(result, toThisGlyph=True) compatibilityReporterClass = GlyphCompatibilityReporter @staticmethod def _checkPairs(object1, object2, reporter, reporterObject): compatibility = object1.isCompatible(object2)[1] if compatibility.fatal or compatibility.warning: if compatibility.fatal: reporter.fatal = True if compatibility.warning: reporter.warning = True reporterObject.append(compatibility) def isCompatible(self, other): """ Evaluate the interpolation compatibility of this glyph and ``other``. >>> compatible, report = self.isCompatible(otherGlyph) >>> compatible False This will return a :ref:`type-bool` indicating if this glyph is compatible with ``other`` and a :class:`GlyphCompatibilityReporter` containing a detailed report about compatibility errors. """ return super(BaseGlyph, self).isCompatible(other, BaseGlyph) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseGlyph.isCompatible`. Subclasses may override this method. """ glyph1 = self glyph2 = other # contour count if len(self.contours) != len(glyph2.contours): reporter.fatal = True reporter.contourCountDifference = True # contour pairs for i in range(min(len(glyph1), len(glyph2))): contour1 = glyph1[i] contour2 = glyph2[i] self._checkPairs(contour1, contour2, reporter, reporter.contours) # component count if len(glyph1.components) != len(glyph2.components): reporter.fatal = True reporter.componentCountDifference = True # component check component_diff = [] selfComponents = [component.baseGlyph for component in glyph1.components] otherComponents = [component.baseGlyph for component in glyph2.components] for index, (left, right) in enumerate( zip_longest(selfComponents, otherComponents) ): if left != right: component_diff.append((index, left, right)) if component_diff: reporter.warning = True reporter.componentDifferences = component_diff if not reporter.componentCountDifference and set(selfComponents) == set( otherComponents ): reporter.componentOrderDifference = True selfComponents_counted_set = collections.Counter(selfComponents) otherComponents_counted_set = collections.Counter(otherComponents) missing_from_glyph1 = ( otherComponents_counted_set - selfComponents_counted_set ) if missing_from_glyph1: reporter.fatal = True reporter.componentsMissingFromGlyph1 = sorted( missing_from_glyph1.elements() ) missing_from_glyph2 = ( selfComponents_counted_set - otherComponents_counted_set ) if missing_from_glyph2: reporter.fatal = True reporter.componentsMissingFromGlyph2 = sorted( missing_from_glyph2.elements() ) # guideline count if len(self.guidelines) != len(glyph2.guidelines): reporter.warning = True reporter.guidelineCountDifference = True # guideline check selfGuidelines = [] otherGuidelines = [] for source, names in ((self, selfGuidelines), (other, otherGuidelines)): for i, guideline in enumerate(source.guidelines): names.append((guideline.name, i)) guidelines1 = set(selfGuidelines) guidelines2 = set(otherGuidelines) if len(guidelines1.difference(guidelines2)) != 0: reporter.warning = True reporter.guidelinesMissingFromGlyph2 = list( guidelines1.difference(guidelines2)) if len(guidelines2.difference(guidelines1)) != 0: reporter.warning = True reporter.guidelinesMissingFromGlyph1 = list( guidelines2.difference(guidelines1)) # anchor count if len(self.anchors) != len(glyph2.anchors): reporter.warning = True reporter.anchorCountDifference = True # anchor check anchor_diff = [] selfAnchors = [anchor.name for anchor in glyph1.anchors] otherAnchors = [anchor.name for anchor in glyph2.anchors] for index, (left, right) in enumerate(zip_longest(selfAnchors, otherAnchors)): if left != right: anchor_diff.append((index, left, right)) if anchor_diff: reporter.warning = True reporter.anchorDifferences = anchor_diff if not reporter.anchorCountDifference and set(selfAnchors) == set( otherAnchors ): reporter.anchorOrderDifference = True selfAnchors_counted_set = collections.Counter(selfAnchors) otherAnchors_counted_set = collections.Counter(otherAnchors) missing_from_glyph1 = otherAnchors_counted_set - selfAnchors_counted_set if missing_from_glyph1: reporter.anchorsMissingFromGlyph1 = sorted( missing_from_glyph1.elements() ) missing_from_glyph2 = selfAnchors_counted_set - otherAnchors_counted_set if missing_from_glyph2: reporter.anchorsMissingFromGlyph2 = sorted( missing_from_glyph2.elements() ) # ------------ # Data Queries # ------------ def pointInside(self, point): """ Determine if ``point`` is in the black or white of the glyph. >>> glyph.pointInside((40, 65)) True ``point`` must be a :ref:`type-coordinate`. """ point = normalizers.normalizeCoordinateTuple(point) return self._pointInside(point) def _pointInside(self, point): """ Subclasses may override this method. """ from fontTools.pens.pointInsidePen import PointInsidePen pen = PointInsidePen(glyphSet=None, testPoint=point, evenOdd=False) self.draw(pen) return pen.getResult() bounds = dynamicProperty( "bounds", """ The bounds of the glyph in the form ``(x minimum, y minimum, x maximum, y maximum)`` or, in the case of empty glyphs ``None``. >>> glyph.bounds (10, 30, 765, 643) """ ) def _get_base_bounds(self): value = self._get_bounds() if value is not None: value = normalizers.normalizeBoundingBox(value) return value def _get_bounds(self): """ Subclasses may override this method. """ from fontTools.pens.boundsPen import BoundsPen pen = BoundsPen(self.layer) self.draw(pen) return pen.bounds area = dynamicProperty( "area", """ The area of the glyph as a :ref:`type-int-float` or, in the case of empty glyphs ``None``. >>> glyph.area 583 """ ) def _get_base_area(self): value = self._get_area() if value is not None: value = normalizers.normalizeArea(value) return value def _get_area(self): """ Subclasses may override this method. """ from fontTools.pens.areaPen import AreaPen pen = AreaPen(self.layer) self.draw(pen) return abs(pen.value) # ----------------- # Layer Interaction # ----------------- layers = dynamicProperty( "layers", """ Immutable tuple of the glyph's layers. >>> glyphLayers = glyph.layers This will return a tuple of all :ref:`type-glyph-layer` in the glyph. """ ) def _get_layers(self, **kwargs): font = self.font if font is None: return tuple() glyphs = [] for layer in font.layers: if self.name in layer: glyphs.append(layer[self.name]) return tuple(glyphs) # get def getLayer(self, name): """ Get the :ref:`type-glyph-layer` with ``name`` in this glyph. >>> glyphLayer = glyph.getLayer("foreground") """ name = normalizers.normalizeLayerName(name) return self._getLayer(name) def _getLayer(self, name, **kwargs): """ name will be a string, but there may not be a layer with a name matching the string. If not, a ``ValueError`` must be raised. Subclasses may override this method. """ for glyph in self.layers: if glyph.layer.name == name: return glyph raise ValueError("No layer named '%s' in glyph '%s'." % (name, self.name)) # new def newLayer(self, name): """ Make a new layer with ``name`` in this glyph. >>> glyphLayer = glyph.newLayer("background") This will return the new :ref:`type-glyph-layer`. If the layer already exists in this glyph, it will be cleared. """ layerName = name glyphName = self.name layerName = normalizers.normalizeLayerName(layerName) for glyph in self.layers: if glyph.layer.name == layerName: layer = glyph.layer layer.removeGlyph(glyphName) break glyph = self._newLayer(name=layerName) layer = self.font.getLayer(layerName) # layer._setLayerInGlyph(glyph) return glyph def _newLayer(self, name, **kwargs): """ name will be a string representing a valid layer name. The name will have been tested to make sure that no layer in the glyph already has the name. This must returned the new glyph. Subclasses must override this method. """ self.raiseNotImplementedError() # remove def removeLayer(self, layer): """ Remove ``layer`` from this glyph. >>> glyph.removeLayer("background") Layer can be a :ref:`type-glyph-layer` or a :ref:`type-string` representing a layer name. """ if isinstance(layer, BaseGlyph): layer = layer.layer.name layerName = layer layerName = normalizers.normalizeLayerName(layerName) if self._getLayer(layerName).layer.name == layerName: self._removeLayer(layerName) def _removeLayer(self, name, **kwargs): """ name will be a valid layer name. It will represent an existing layer in the font. Subclasses may override this method. """ self.raiseNotImplementedError() # ----- # Image # ----- image = dynamicProperty( "base_image", "The :class:`BaseImage` for the glyph." ) def _get_base_image(self): image = self._get_image() if image.glyph is None: image.glyph = self return image def _get_image(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def addImage(self, path=None, data=None, scale=None, position=None, color=None): """ Set the image in the glyph. This will return the assigned :class:`BaseImage`. The image data can be defined via ``path`` to an image file: >>> image = glyph.addImage(path="/path/to/my/image.png") The image data can be defined with raw image data via ``data``. >>> image = glyph.addImage(data=someImageData) If ``path`` and ``data`` are both provided, a :class:`FontPartsError` will be raised. The supported image formats will vary across environments. Refer to :class:`BaseImage` for complete details. ``scale`` indicates the x and y scale values that should be applied to the image. It must be a :ref:`type-scale` value or ``None``. >>> image = glyph.addImage(path="/p/t/image.png", scale=(0.5, 1.0)) ``position`` indicates the x and y location of the lower left point of the image. >>> image = glyph.addImage(path="/p/t/image.png", position=(10, 20)) ``color`` indicates the color to be applied to the image. It must be a :ref:`type-color` or ``None``. >>> image = glyph.addImage(path="/p/t/image.png", color=(1, 0, 0, 0.5)) """ if path is not None and data is not None: raise FontPartsError("Only path or data may be defined, not both.") if scale is None: scale = (1, 1) if position is None: position = (0, 0) scale = normalizers.normalizeTransformationScale(scale) position = normalizers.normalizeTransformationOffset(position) if color is not None: color = normalizers.normalizeColor(color) sx, sy = scale ox, oy = position transformation = (sx, 0, 0, sy, ox, oy) if path is not None: if not os.path.exists(path): raise IOError("No image located at '%s'." % path) f = open(path, "rb") data = f.read() f.close() self._addImage(data=data, transformation=transformation, color=color) return self.image def _addImage(self, data, transformation=None, color=None): """ data will be raw, unnormalized image data. Each environment may have different possible formats, so this is unspecified. transformation will be a valid transformation matrix. color will be a color tuple or None. This must return an Image object. Assigning it to the glyph will be handled by the base class. Subclasses must override this method. """ self.raiseNotImplementedError() def clearImage(self): """ Remove the image from the glyph. >>> glyph.clearImage() """ if self.image is not None: self._clearImage() def _clearImage(self, **kwargs): """ Subclasses must override this method. """ self.raiseNotImplementedError() # ---------- # Mark color # ---------- markColor = dynamicProperty( "base_markColor", """ The glyph's mark color. >>> glyph.markColor (1, 0, 0, 0.5) >>> glyph.markColor = None The value may be a :ref:`type-color` or ``None``. """ ) def _get_base_markColor(self): value = self._get_markColor() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_markColor(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_markColor(value) def _get_markColor(self): """ Return the mark color value as a color tuple or None. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_markColor(self, value): """ value will be a color tuple or None. Subclasses must override this method. """ self.raiseNotImplementedError() # ---- # Note # ---- note = dynamicProperty( "base_note", """ The glyph's note. >>> glyph.note "P.B. said this looks 'awesome.'" >>> glyph.note = "P.B. said this looks 'AWESOME.'" The value may be a :ref:`type-string` or ``None``. """ ) def _get_base_note(self): value = self._get_note() if value is not None: value = normalizers.normalizeGlyphNote(value) return value def _set_base_note(self, value): if value is not None: value = normalizers.normalizeGlyphNote(value) self._set_note(value) def _get_note(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_note(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # --- # Lib # --- lib = dynamicProperty( "base_lib", """ The :class:`BaseLib` for the glyph. >>> lib = glyph.lib """ ) def _get_base_lib(self): lib = self._get_lib() lib.glyph = self return lib def _get_lib(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() # -------- # Temp Lib # -------- tempLib = dynamicProperty( "base_tempLib", """ The :class:`BaseLib` for the glyph. >>> tempLib = glyph.tempLib """ ) def _get_base_tempLib(self): lib = self._get_tempLib() lib.glyph = self return lib def _get_tempLib(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() # --- # API # --- def isEmpty(self): """ This will return :ref:`type-bool` indicating if there are contours and/or components in the glyph. >>> glyph.isEmpty() Note: This method only checks for the presence of contours and components. Other attributes (guidelines, anchors, a lib, etc.) will not affect what this method returns. """ if self.contours: return False if self.components: return False return True def loadFromGLIF(self, glifData): """ Reads ``glifData``, in `GLIF format `_, into this glyph. >>> glyph.readGlyphFromString(xmlData) """ self._loadFromGLIF(glifData) def _loadFromGLIF(self, glifData): """ Subclasses must override this method. """ self.raiseNotImplementedError() def dumpToGLIF(self, glyphFormatVersion=2): """ This will return the glyph's contents as a string in `GLIF format `_. >>> xml = glyph.writeGlyphToString() ``glyphFormatVersion`` must be a :ref:`type-int` that defines the preferred GLIF format version. """ glyphFormatVersion = normalizers.normalizeGlyphFormatVersion( glyphFormatVersion) return self._dumpToGLIF(glyphFormatVersion) def _dumpToGLIF(self, glyphFormatVersion): """ Subclasses must override this method. """ self.raiseNotImplementedError() # --------- # Selection # --------- # contours selectedContours = dynamicProperty( "base_selectedContours", """ An :ref:`type-immutable-list` of contours selected in the glyph. >>> contours = glyph.selectedContours: >>> glyph.selectedContours = otherContours It is possible to use a list of :ref:`type-int` representing contour indexes when setting the selected contours. >>> glyph.selectedContours = [0, 2] """ ) def _get_base_selectedContours(self): selected = tuple([normalizers.normalizeContour(contour) for contour in self._get_selectedContours()]) return selected def _get_selectedContours(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.contours) def _set_base_selectedContours(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeIndex(i) else: i = normalizers.normalizeContour(i) normalized.append(i) self._set_selectedContours(normalized) def _set_selectedContours(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.contours, value) # components selectedComponents = dynamicProperty( "base_selectedComponents", """ An :ref:`type-immutable-list` of components selected in the glyph. >>> components = glyph.selectedComponents: >>> glyph.selectedComponents = otherComponents It is possible to use a list of :ref:`type-int` representing component indexes when setting the selected components. >>> glyph.selectedComponents = [0, 2] """ ) def _get_base_selectedComponents(self): selected = tuple([normalizers.normalizeComponent(component) for component in self._get_selectedComponents()]) return selected def _get_selectedComponents(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.components) def _set_base_selectedComponents(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeIndex(i) else: i = normalizers.normalizeComponent(i) normalized.append(i) self._set_selectedComponents(normalized) def _set_selectedComponents(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.components, value) # anchors selectedAnchors = dynamicProperty( "base_selectedAnchors", """ An :ref:`type-immutable-list` of anchors selected in the glyph. >>> anchors = glyph.selectedAnchors: >>> glyph.selectedAnchors = otherAnchors It is possible to use a list of :ref:`type-int` representing anchor indexes when setting the selected anchors. >>> glyph.selectedAnchors = [0, 2] """ ) def _get_base_selectedAnchors(self): selected = tuple([normalizers.normalizeAnchor(anchor) for anchor in self._get_selectedAnchors()]) return selected def _get_selectedAnchors(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.anchors) def _set_base_selectedAnchors(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeIndex(i) else: i = normalizers.normalizeAnchor(i) normalized.append(i) self._set_selectedAnchors(normalized) def _set_selectedAnchors(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.anchors, value) # guidelines selectedGuidelines = dynamicProperty( "base_selectedGuidelines", """ An :ref:`type-immutable-list` of guidelines selected in the glyph. >>> guidelines = glyph.selectedGuidelines: >>> glyph.selectedGuidelines = otherGuidelines It is possible to use a list of :ref:`type-int` representing guidelines indexes when setting the selected guidelines. >>> glyph.selectedGuidelines = [0, 2] """ ) def _get_base_selectedGuidelines(self): selected = tuple([normalizers.normalizeGuideline(guideline) for guideline in self._get_selectedGuidelines()]) return selected def _get_selectedGuidelines(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.guidelines) def _set_base_selectedGuidelines(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeIndex(i) else: i = normalizers.normalizeGuideline(i) normalized.append(i) self._set_selectedGuidelines(normalized) def _set_selectedGuidelines(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.guidelines, value) robotools-fontParts-26e8b8c/Lib/fontParts/base/groups.py000066400000000000000000000256131477533125200234460ustar00rootroot00000000000000from fontParts.base.base import BaseDict, dynamicProperty, reference from fontParts.base import normalizers from fontParts.base.deprecated import DeprecatedGroups, RemovedGroups class BaseGroups(BaseDict, DeprecatedGroups, RemovedGroups): """ A Groups object. This object normally created as part of a :class:`BaseFont`. An orphan Groups object can be created like this:: >>> groups = RGroups() This object behaves like a Python dictionary. Most of the dictionary functionality comes from :class:`BaseDict`, look at that object for the required environment implementation details. Groups uses :func:`normalizers.normalizeGroupKey` to normalize the key of the ``dict``, and :func:`normalizers.normalizeGroupValue` to normalize the value of the ``dict``. """ keyNormalizer = normalizers.normalizeGroupKey valueNormalizer = normalizers.normalizeGroupValue def _reprContents(self): contents = [] if self.font is not None: contents.append("for font") contents += self.font._reprContents() return contents # ------- # Parents # ------- # Font _font = None font = dynamicProperty("font", "The Groups' parent :class:`BaseFont`.") def _get_font(self): if self._font is None: return None return self._font() def _set_font(self, font): if self._font is not None and self._font != font: raise AssertionError("font for groups already set and is not same as font") if font is not None: font = reference(font) self._font = font # --------- # Searching # --------- def findGlyph(self, glyphName): """ Returns a ``list`` of the group or groups associated with **glyphName**. **glyphName** will be an :ref:`type-string`. If no group is found to contain **glyphName** an empty ``list`` will be returned. :: >>> font.groups.findGlyph("A") ["A_accented"] """ glyphName = normalizers.normalizeGlyphName(glyphName) groupNames = self._findGlyph(glyphName) groupNames = [self.keyNormalizer.__func__( groupName) for groupName in groupNames] return groupNames def _findGlyph(self, glyphName): """ This is the environment implementation of :meth:`BaseGroups.findGlyph`. **glyphName** will be an :ref:`type-string`. Subclasses may override this method. """ found = [] for key, groupList in self.items(): if glyphName in groupList: found.append(key) return found # -------------- # Kerning Groups # -------------- side1KerningGroups = dynamicProperty( "base_side1KerningGroups", """ All groups marked as potential side 1 kerning members. >>> side1Groups = groups.side1KerningGroups The value will be a :ref:`dict` with :ref:`string` keys representing group names and :ref:`tuple` contaning glyph names. """ ) def _get_base_side1KerningGroups(self): kerningGroups = self._get_side1KerningGroups() normalized = {} for name, members in kerningGroups.items(): name = normalizers.normalizeGroupKey(name) members = normalizers.normalizeGroupValue(members) normalized[name] = members return normalized def _get_side1KerningGroups(self): """ Subclasses may override this method. """ found = {} for name, contents in self.items(): if name.startswith("public.kern1."): found[name] = contents return found side2KerningGroups = dynamicProperty( "base_side2KerningGroups", """ All groups marked as potential side 1 kerning members. >>> side2Groups = groups.side2KerningGroups The value will be a :ref:`dict` with :ref:`string` keys representing group names and :ref:`tuple` contaning glyph names. """ ) def _get_base_side2KerningGroups(self): kerningGroups = self._get_side2KerningGroups() normalized = {} for name, members in kerningGroups.items(): name = normalizers.normalizeGroupKey(name) members = normalizers.normalizeGroupValue(members) normalized[name] = members return normalized def _get_side2KerningGroups(self): """ Subclasses may override this method. """ found = {} for name, contents in self.items(): if name.startswith("public.kern2."): found[name] = contents return found # --------------------- # RoboFab Compatibility # --------------------- def remove(self, groupName): """ Removes a group from the Groups. **groupName** will be a :ref:`type-string` that is the group name to be removed. This is a backwards compatibility method. """ del self[groupName] def asDict(self): """ Return the Groups as a ``dict``. This is a backwards compatibility method. """ d = {} for k, v in self.items(): d[k] = v return d # ------------------- # Inherited Functions # ------------------- def __contains__(self, groupName): """ Tests to see if a group name is in the Groups. **groupName** will be a :ref:`type-string`. This returns a ``bool`` indicating if the **groupName** is in the Groups. :: >>> "myGroup" in font.groups True """ return super(BaseGroups, self).__contains__(groupName) def __delitem__(self, groupName): """ Removes **groupName** from the Groups. **groupName** is a :ref:`type-string`.:: >>> del font.groups["myGroup"] """ super(BaseGroups, self).__delitem__(groupName) def __getitem__(self, groupName): """ Returns the contents of the named group. **groupName** is a :ref:`type-string`. The returned value will be a :ref:`type-immutable-list` of the group contents.:: >>> font.groups["myGroup"] ("A", "B", "C") It is important to understand that any changes to the returned group contents will not be reflected in the Groups object. If one wants to make a change to the group contents, one should do the following:: >>> group = font.groups["myGroup"] >>> group.remove("A") >>> font.groups["myGroup"] = group """ return super(BaseGroups, self).__getitem__(groupName) def __iter__(self): """ Iterates through the Groups, giving the key for each iteration. The order that the Groups will iterate though is not fixed nor is it ordered.:: >>> for groupName in font.groups: >>> print groupName "myGroup" "myGroup3" "myGroup2" """ return super(BaseGroups, self).__iter__() def __len__(self): """ Returns the number of groups in Groups as an ``int``.:: >>> len(font.groups) 5 """ return super(BaseGroups, self).__len__() def __setitem__(self, groupName, glyphNames): """ Sets the **groupName** to the list of **glyphNames**. **groupName** is the group name as a :ref:`type-string` and **glyphNames** is a ``list`` of glyph names as :ref:`type-string`. >>> font.groups["myGroup"] = ["A", "B", "C"] """ super(BaseGroups, self).__setitem__(groupName, glyphNames) def clear(self): """ Removes all group information from Groups, resetting the Groups to an empty dictionary. :: >>> font.groups.clear() """ super(BaseGroups, self).clear() def get(self, groupName, default=None): """ Returns the contents of the named group. **groupName** is a :ref:`type-string`, and the returned values will either be :ref:`type-immutable-list` of group contents or ``None`` if no group was found. :: >>> font.groups["myGroup"] ("A", "B", "C") It is important to understand that any changes to the returned group contents will not be reflected in the Groups object. If one wants to make a change to the group contents, one should do the following:: >>> group = font.groups["myGroup"] >>> group.remove("A") >>> font.groups["myGroup"] = group """ return super(BaseGroups, self).get(groupName, default) def items(self): """ Returns a list of ``tuple`` of each group name and group members. Group names are :ref:`type-string` and group members are a :ref:`type-immutable-list` of :ref:`type-string`. The initial list will be unordered. >>> font.groups.items() [("myGroup", ("A", "B", "C"), ("myGroup2", ("D", "E", "F"))] """ return super(BaseGroups, self).items() def keys(self): """ Returns a ``list`` of all the group names in Groups. This list will be unordered.:: >>> font.groups.keys() ["myGroup4", "myGroup1", "myGroup5"] """ return super(BaseGroups, self).keys() def pop(self, groupName, default=None): """ Removes the **groupName** from the Groups and returns the list of group members. If no group is found, **default** is returned. **groupName** is a :ref:`type-string`. This must return either **default** or a :ref:`type-immutable-list` of glyph names as :ref:`type-string`. >>> font.groups.pop("myGroup") ("A", "B", "C") """ return super(BaseGroups, self).pop(groupName, default) def update(self, otherGroups): """ Updates the Groups based on **otherGroups**. *otherGroups** is a ``dict`` of groups information. If a group from **otherGroups** is in Groups, the group members will be replaced by the group members from **otherGroups**. If a group from **otherGroups** is not in the Groups, it is added to the Groups. If Groups contain a group name that is not in *otherGroups**, it is not changed. >>> font.groups.update(newGroups) """ super(BaseGroups, self).update(otherGroups) def values(self): """ Returns a ``list`` of each named group's members. This will be a list of lists, the group members will be a :ref:`type-immutable-list` of :ref:`type-string`. The initial list will be unordered. >>> font.groups.items() [("A", "B", "C"), ("D", "E", "F")] """ return super(BaseGroups, self).values() robotools-fontParts-26e8b8c/Lib/fontParts/base/guideline.py000066400000000000000000000324761477533125200241010ustar00rootroot00000000000000import math from fontTools.misc import transform from fontParts.base.base import ( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, PointPositionMixin, IdentifierMixin, dynamicProperty, reference ) from fontParts.base import normalizers from fontParts.base.compatibility import GuidelineCompatibilityReporter from fontParts.base.color import Color from fontParts.base.deprecated import DeprecatedGuideline, RemovedGuideline class BaseGuideline( BaseObject, TransformationMixin, DeprecatedGuideline, RemovedGuideline, PointPositionMixin, InterpolationMixin, IdentifierMixin, SelectionMixin ): """ A guideline object. This object is almost always created with :meth:`BaseGlyph.appendGuideline`. An orphan guideline can be created like this:: >>> guideline = RGuideline() """ copyAttributes = ( "x", "y", "angle", "name", "color" ) def _reprContents(self): contents = [] if self.name is not None: contents.append("'%s'" % self.name) if self.layer is not None: contents.append("('%s')" % self.layer.name) return contents # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The guideline's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._font is not None: raise AssertionError("font for guideline already set") if self._glyph is not None: raise AssertionError("glyph for guideline already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Layer layer = dynamicProperty("layer", "The guideline's parent :class:`BaseLayer`.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # Font _font = None font = dynamicProperty("font", "The guideline's parent :class:`BaseFont`.") def _get_font(self): if self._font is not None: return self._font() elif self._glyph is not None: return self.glyph.font return None def _set_font(self, font): if self._font is not None: raise AssertionError("font for guideline already set") if self._glyph is not None: raise AssertionError("glyph for guideline already set") if font is not None: font = reference(font) self._font = font # -------- # Position # -------- # x x = dynamicProperty( "base_x", """ The x coordinate of the guideline. It must be an :ref:`type-int-float`. :: >>> guideline.x 100 >>> guideline.x = 101 """ ) def _get_base_x(self): value = self._get_x() if value is None: return 0 value = normalizers.normalizeX(value) return value def _set_base_x(self, value): if value is None: value = 0 else: value = normalizers.normalizeX(value) self._set_x(value) def _get_x(self): """ This is the environment implementation of :attr:`BaseGuideline.x`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_x(self, value): """ This is the environment implementation of :attr:`BaseGuideline.x`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # y y = dynamicProperty( "base_y", """ The y coordinate of the guideline. It must be an :ref:`type-int-float`. :: >>> guideline.y 100 >>> guideline.y = 101 """ ) def _get_base_y(self): value = self._get_y() if value is None: return 0 value = normalizers.normalizeY(value) return value def _set_base_y(self, value): if value is None: value = 0 else: value = normalizers.normalizeY(value) self._set_y(value) def _get_y(self): """ This is the environment implementation of :attr:`BaseGuideline.y`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_y(self, value): """ This is the environment implementation of :attr:`BaseGuideline.y`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # angle angle = dynamicProperty( "base_angle", """ The angle of the guideline. It must be an :ref:`type-angle`. Please check how :func:`normalizers.normalizeRotationAngle` handles the angle. There is a special case, when angle is ``None``. If so, when x and y are not 0, the angle will be 0. If x is 0 but y is not, the angle will be 0. If y is 0 and x is not, the angle will be 90. If both x and y are 0, the angle will be 0. :: >>> guideline.angle 45.0 >>> guideline.angle = 90 """ ) def _get_base_angle(self): value = self._get_angle() if value is None: if self._get_x() != 0 and self._get_y() != 0: value = 0 elif self._get_x() != 0 and self._get_y() == 0: value = 90 elif self._get_x() == 0 and self._get_y() != 0: value = 0 else: value = 0 value = normalizers.normalizeRotationAngle(value) return value def _set_base_angle(self, value): if value is None: if self._get_x() != 0 and self._get_y() != 0: value = 0 elif self._get_x() != 0 and self._get_y() == 0: value = 90 elif self._get_x() == 0 and self._get_y() != 0: value = 0 else: value = 0 value = normalizers.normalizeRotationAngle(value) self._set_angle(value) def _get_angle(self): """ This is the environment implementation of :attr:`BaseGuideline.angle`. This must return an :ref:`type-angle`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_angle(self, value): """ This is the environment implementation of :attr:`BaseGuideline.angle`. **value** will be an :ref:`type-angle`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Identification # -------------- # index index = dynamicProperty( "base_index", """ The index of the guideline within the ordered list of the parent glyph's guidelines. This attribute is read only. :: >>> guideline.index 0 """ ) def _get_base_index(self): value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Get the guideline's index. This must return an ``int``. Subclasses may override this method. """ glyph = self.glyph if glyph is not None: parent = glyph else: parent = self.font if parent is None: return None return parent.guidelines.index(self) # name name = dynamicProperty( "base_name", """ The name of the guideline. This will be a :ref:`type-string` or ``None``. >>> guideline.name 'my guideline' >>> guideline.name = None """ ) def _get_base_name(self): value = self._get_name() if value is not None: value = normalizers.normalizeGuidelineName(value) return value def _set_base_name(self, value): if value is not None: value = normalizers.normalizeGuidelineName(value) self._set_name(value) def _get_name(self): """ This is the environment implementation of :attr:`BaseGuideline.name`. This must return a :ref:`type-string` or ``None``. The returned value will be normalized with :func:`normalizers.normalizeGuidelineName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value): """ This is the environment implementation of :attr:`BaseGuideline.name`. **value** will be a :ref:`type-string` or ``None``. It will have been normalized with :func:`normalizers.normalizeGuidelineName`. Subclasses must override this method. """ self.raiseNotImplementedError() # color color = dynamicProperty( "base_color", """" The guideline's color. This will be a :ref:`type-color` or ``None``. :: >>> guideline.color None >>> guideline.color = (1, 0, 0, 0.5) """ ) def _get_base_color(self): value = self._get_color() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_color(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_color(value) def _get_color(self): """ This is the environment implementation of :attr:`BaseGuideline.color`. This must return a :ref:`type-color` or ``None``. The returned value will be normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_color(self, value): """ This is the environment implementation of :attr:`BaseGuideline.color`. **value** will be a :ref:`type-color` or ``None``. It will have been normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ This is the environment implementation of :meth:`BaseGuideline.transformBy`. **matrix** will be a :ref:`type-transformation`. that has been normalized with :func:`normalizers.normalizeTransformationMatrix`. Subclasses may override this method. """ t = transform.Transform(*matrix) # coordinates x, y = t.transformPoint((self.x, self.y)) self.x = x self.y = y # angle angle = math.radians(-self.angle) dx = math.cos(angle) dy = math.sin(angle) tdx, tdy = t.transformPoint((dx, dy)) ta = math.atan2(tdy - t[5], tdx - t[4]) self.angle = -math.degrees(ta) # ------------- # Interpolation # ------------- compatibilityReporterClass = GuidelineCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherGuideline) >>> compatible True >>> compatible [Warning] Guideline: "xheight" + "cap_height" [Warning] Guideline: "xheight" has name xheight | "cap_height" has name cap_height This will return a ``bool`` indicating if the guideline is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseGuideline, self).isCompatible(other, BaseGuideline) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseGuideline.isCompatible`. Subclasses may override this method. """ guideline1 = self guideline2 = other # guideline names if guideline1.name != guideline2.name: reporter.nameDifference = True reporter.warning = True # ------------- # Normalization # ------------- def round(self): """ Round the guideline's coordinate. >>> guideline.round() This applies to the following: * x * y It does not apply to * angle """ self._round() def _round(self, **kwargs): """ This is the environment implementation of :meth:`BaseGuideline.round`. Subclasses may override this method. """ self.x = normalizers.normalizeVisualRounding(self.x) self.y = normalizers.normalizeVisualRounding(self.y) robotools-fontParts-26e8b8c/Lib/fontParts/base/image.py000066400000000000000000000172521477533125200232110ustar00rootroot00000000000000from fontTools.misc import transform from fontParts.base.base import ( BaseObject, TransformationMixin, PointPositionMixin, SelectionMixin, dynamicProperty, reference ) from fontParts.base import normalizers from fontParts.base.color import Color from fontParts.base.deprecated import DeprecatedImage, RemovedImage class BaseImage( BaseObject, TransformationMixin, PointPositionMixin, SelectionMixin, DeprecatedImage, RemovedImage ): copyAttributes = ( "transformation", "color", "data" ) def _reprContents(self): contents = [ "offset='({x}, {y})'".format(x=self.offset[0], y=self.offset[1]), ] if self.color: contents.append("color=%r" % str(self.color)) if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() return contents def __bool__(self): if self.data is None: return False elif len(self.data) == 0: return False else: return True __nonzero__ = __bool__ # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The image's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._glyph is not None: raise AssertionError("glyph for image already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Layer layer = dynamicProperty("layer", "The image's parent :class:`BaseLayer`.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The image's parent :class:`BaseFont`.") def _get_font(self): if self._glyph is None: return None return self.glyph.font # ---------- # Attributes # ---------- # Transformation transformation = dynamicProperty( "base_transformation", """ The image's :ref:`type-transformation`. This defines the image's position, scale, and rotation. :: >>> image.transformation (1, 0, 0, 1, 0, 0) >>> image.transformation = (2, 0, 0, 2, 100, -50) """ ) def _get_base_transformation(self): value = self._get_transformation() value = normalizers.normalizeTransformationMatrix(value) return value def _set_base_transformation(self, value): value = normalizers.normalizeTransformationMatrix(value) self._set_transformation(value) def _get_transformation(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_transformation(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() offset = dynamicProperty( "base_offset", """ The image's offset. This is a shortcut to the offset values in :attr:`transformation`. This must be an iterable containing two :ref:`type-int-float` values defining the x and y values to offset the image by. :: >>> image.offset (0, 0) >>> image.offset = (100, -50) """ ) def _get_base_offset(self): value = self._get_offset() value = normalizers.normalizeTransformationOffset(value) return value def _set_base_offset(self, value): value = normalizers.normalizeTransformationOffset(value) self._set_offset(value) def _get_offset(self): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation return (ox, oy) def _set_offset(self, value): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation ox, oy = value self.transformation = (sx, sxy, syx, sy, ox, oy) scale = dynamicProperty( "base_scale", """ The image's scale. This is a shortcut to the scale values in :attr:`transformation`. This must be an iterable containing two :ref:`type-int-float` values defining the x and y values to scale the image by. :: >>> image.scale (1, 1) >>> image.scale = (2, 2) """ ) def _get_base_scale(self): value = self._get_scale() value = normalizers.normalizeTransformationScale(value) return value def _set_base_scale(self, value): value = normalizers.normalizeTransformationScale(value) self._set_scale(value) def _get_scale(self): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation return (sx, sy) def _set_scale(self, value): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation sx, sy = value self.transformation = (sx, sxy, syx, sy, ox, oy) # Color color = dynamicProperty( "base_color", """ The image's color. This will be a :ref:`type-color` or ``None``. :: >>> image.color None >>> image.color = (1, 0, 0, 0.5) """ ) def _get_base_color(self): value = self._get_color() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_color(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_color(value) def _get_color(self): """ Return the color value as a color tuple or None. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_color(self, value): """ value will be a color tuple or None. Subclasses must override this method. """ self.raiseNotImplementedError() # Data data = dynamicProperty( "data", """ The image's raw byte data. The possible formats are defined by each environment. """ ) def _get_base_data(self): return self._get_data() def _set_base_data(self, value): self._set_data(value) def _get_data(self): """ This must return raw byte data. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_data(self, value): """ value will be raw byte data. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ t = transform.Transform(*matrix) transformation = t.transform(self.transformation) self.transformation = tuple(transformation) # ------------- # Normalization # ------------- def round(self): """ Round offset coordinates. """ self._round() def _round(self): """ Subclasses may override this method. """ x, y = self.offset x = normalizers.normalizeVisualRounding(x) y = normalizers.normalizeVisualRounding(y) self.offset = (x, y) robotools-fontParts-26e8b8c/Lib/fontParts/base/info.py000066400000000000000000000254311477533125200230600ustar00rootroot00000000000000from fontParts.base.base import ( BaseObject, dynamicProperty, interpolate, reference ) from fontParts.base import normalizers from fontParts.base.errors import FontPartsError from fontParts.base.deprecated import DeprecatedInfo, RemovedInfo class BaseInfo(BaseObject, DeprecatedInfo, RemovedInfo): from fontTools.ufoLib import fontInfoAttributesVersion3 copyAttributes = set(fontInfoAttributesVersion3) copyAttributes.remove("guidelines") copyAttributes = tuple(copyAttributes) def _reprContents(self): contents = [] if self.font is not None: contents.append("for font") contents += self.font._reprContents() return contents # ------- # Parents # ------- # Font _font = None font = dynamicProperty("font", "The info's parent font.") def _get_font(self): if self._font is None: return None return self._font() def _set_font(self, font): if self._font is not None and self._font != font: raise AssertionError("font for info already set and is not same as font") if font is not None: font = reference(font) self._font = font # ---------- # Validation # ---------- @staticmethod def _validateFontInfoAttributeValue(attr, value): from fontTools.ufoLib import validateFontInfoVersion3ValueForAttribute valid = validateFontInfoVersion3ValueForAttribute(attr, value) if not valid: raise ValueError("Invalid value %s for attribute '%s'." % (value, attr)) return value # ---------- # Attributes # ---------- # has def __hasattr__(self, attr): from fontTools.ufoLib import fontInfoAttributesVersion3 if attr in fontInfoAttributesVersion3: return True return super(BaseInfo, self).__hasattr__(attr) # get def __getattribute__(self, attr): from fontTools.ufoLib import fontInfoAttributesVersion3 if attr != "guidelines" and attr in fontInfoAttributesVersion3: value = self._getAttr(attr) if value is not None: value = self._validateFontInfoAttributeValue(attr, value) return value return super(BaseInfo, self).__getattribute__(attr) def _getAttr(self, attr): """ Subclasses may override this method. If a subclass does not override this method, it must implement '_get_attributeName' methods for all Info methods. """ meth = "_get_%s" % attr if not hasattr(self, meth): raise AttributeError("No getter for attribute '%s'." % attr) meth = getattr(self, meth) value = meth() return value # set def __setattr__(self, attr, value): from fontTools.ufoLib import fontInfoAttributesVersion3 if attr != "guidelines" and attr in fontInfoAttributesVersion3: if value is not None: value = self._validateFontInfoAttributeValue(attr, value) return self._setAttr(attr, value) return super(BaseInfo, self).__setattr__(attr, value) def _setAttr(self, attr, value): """ Subclasses may override this method. If a subclass does not override this method, it must implement '_set_attributeName' methods for all Info methods. """ meth = "_set_%s" % attr if not hasattr(self, meth): raise AttributeError("No setter for attribute '%s'." % attr) meth = getattr(self, meth) meth(value) # ------------- # Normalization # ------------- def round(self): """ Round the following attributes to integers: - unitsPerEm - descender - xHeight - capHeight - ascender - openTypeHeadLowestRecPPEM - openTypeHheaAscender - openTypeHheaDescender - openTypeHheaLineGap - openTypeHheaCaretSlopeRise - openTypeHheaCaretSlopeRun - openTypeHheaCaretOffset - openTypeOS2WidthClass - openTypeOS2WeightClass - openTypeOS2TypoAscender - openTypeOS2TypoDescender - openTypeOS2TypoLineGap - openTypeOS2WinAscent - openTypeOS2WinDescent - openTypeOS2SubscriptXSize - openTypeOS2SubscriptYSize - openTypeOS2SubscriptXOffset - openTypeOS2SubscriptYOffset - openTypeOS2SuperscriptXSize - openTypeOS2SuperscriptYSize - openTypeOS2SuperscriptXOffset - openTypeOS2SuperscriptYOffset - openTypeOS2StrikeoutSize - openTypeOS2StrikeoutPosition - openTypeVheaVertTypoAscender - openTypeVheaVertTypoDescender - openTypeVheaVertTypoLineGap - openTypeVheaCaretSlopeRise - openTypeVheaCaretSlopeRun - openTypeVheaCaretOffset - postscriptSlantAngle - postscriptUnderlineThickness - postscriptUnderlinePosition - postscriptBlueValues - postscriptOtherBlues - postscriptFamilyBlues - postscriptFamilyOtherBlues - postscriptStemSnapH - postscriptStemSnapV - postscriptBlueFuzz - postscriptBlueShift - postscriptDefaultWidthX - postscriptNominalWidthX """ self._round() def _round(self, **kwargs): """ Subclasses may override this method. """ from fontMath.mathFunctions import setRoundIntegerFunction setRoundIntegerFunction(normalizers.normalizeVisualRounding) mathInfo = self._toMathInfo(guidelines=False) mathInfo = mathInfo.round() self._fromMathInfo(mathInfo, guidelines=False) # -------- # Updating # -------- def update(self, other): """ Update this object with the values from **otherInfo**. """ self._update(other) def _update(self, other): """ Subclasses may override this method. """ from fontTools.ufoLib import fontInfoAttributesVersion3 for attr in fontInfoAttributesVersion3: if attr == "guidelines": continue value = getattr(other, attr) setattr(self, attr, value) # ------------- # Interpolation # ------------- def toMathInfo(self, guidelines=True): """ Returns the info as an object that follows the `MathGlyph protocol `_. >>> mg = font.info.toMathInfo() """ return self._toMathInfo(guidelines=guidelines) def fromMathInfo(self, mathInfo, guidelines=True): """ Replaces the contents of this info object with the contents of ``mathInfo``. >>> font.fromMathInfo(mg) ``mathInfo`` must be an object following the `MathInfo protocol `_. """ return self._fromMathInfo(mathInfo, guidelines=guidelines) def _toMathInfo(self, guidelines=True): """ Subclasses may override this method. """ import fontMath # A little trickery is needed here because MathInfo # handles font level guidelines. Those are not in this # object so we temporarily fake them just enough for # MathInfo and then move them back to the proper place. self.guidelines = [] if guidelines: for guideline in self.font.guidelines: d = dict( x=guideline.x, y=guideline.y, angle=guideline.angle, name=guideline.name, identifier=guideline.identifier, color=guideline.color ) self.guidelines.append(d) info = fontMath.MathInfo(self) del self.guidelines return info def _fromMathInfo(self, mathInfo, guidelines=True): """ Subclasses may override this method. """ mathInfo.extractInfo(self) font = self.font if guidelines: for guideline in mathInfo.guidelines: font.appendGuideline( position=(guideline["x"], guideline["y"]), angle=guideline["angle"], name=guideline["name"], color=guideline["color"] # XXX identifier is lost ) def interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True): """ Interpolate all pairs between minInfo and maxInfo. The interpolation occurs on a 0 to 1.0 range where minInfo is located at 0 and maxInfo is located at 1.0. factor is the interpolation value. It may be less than 0 and greater than 1.0. It may be a number (integer, float) or a tuple of two numbers. If it is a tuple, the first number indicates the x factor and the second number indicates the y factor. round indicates if the result should be rounded to integers. suppressError indicates if incompatible data should be ignored or if an error should be raised when such incompatibilities are found. """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minInfo, BaseInfo): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, minInfo.__class__.__name__)) if not isinstance(maxInfo, BaseInfo): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, maxInfo.__class__.__name__)) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate(factor, minInfo, maxInfo, round=round, suppressError=suppressError) def _interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True): """ Subclasses may override this method. """ from fontMath.mathFunctions import setRoundIntegerFunction setRoundIntegerFunction(normalizers.normalizeVisualRounding) minInfo = minInfo._toMathInfo() maxInfo = maxInfo._toMathInfo() result = interpolate(minInfo, maxInfo, factor) if result is None and not suppressError: raise FontPartsError(("Info from font '%s' and font '%s' could not be " "interpolated.") % (minInfo.font.name, maxInfo.font.name)) if round: result = result.round() self._fromMathInfo(result) robotools-fontParts-26e8b8c/Lib/fontParts/base/kerning.py000066400000000000000000000361711477533125200235650ustar00rootroot00000000000000from fontParts.base.base import ( BaseDict, dynamicProperty, interpolate, reference ) from fontParts.base import normalizers from fontParts.base.deprecated import DeprecatedKerning, RemovedKerning class BaseKerning(BaseDict, DeprecatedKerning, RemovedKerning): """ A Kerning object. This object normally created as part of a :class:`BaseFont`. An orphan Kerning object can be created like this:: >>> groups = RKerning() This object behaves like a Python dictionary. Most of the dictionary functionality comes from :class:`BaseDict`, look at that object for the required environment implementation details. Kerning uses :func:`normalizers.normalizeKerningKey` to normalize the key of the ``dict``, and :func:`normalizers.normalizeKerningValue` to normalize the the value of the ``dict``. """ keyNormalizer = normalizers.normalizeKerningKey valueNormalizer = normalizers.normalizeKerningValue def _reprContents(self): contents = [] if self.font is not None: contents.append("for font") contents += self.font._reprContents() return contents # ------- # Parents # ------- # Font _font = None font = dynamicProperty("font", "The Kerning's parent :class:`BaseFont`.") def _get_font(self): if self._font is None: return None return self._font() def _set_font(self, font): if self._font is not None and self._font() != font: raise AssertionError("font for kerning already set and is not same as font") if font is not None: font = reference(font) self._font = font # -------------- # Transformation # -------------- def scaleBy(self, factor): """ Scales all kerning values by **factor**. **factor** will be an :ref:`type-int-float`, ``tuple`` or ``list``. The first value of the **factor** will be used to scale the kerning values. >>> myKerning.scaleBy(2) >>> myKerning.scaleBy((2,3)) """ factor = normalizers.normalizeTransformationScale(factor) self._scale(factor) def _scale(self, factor): """ This is the environment implementation of :meth:`BaseKerning.scaleBy`. **factor** will be a ``tuple``. Subclasses may override this method. """ factor = factor[0] for k, v in self.items(): v *= factor self[k] = v # ------------- # Normalization # ------------- def round(self, multiple=1): """ Rounds the kerning values to increments of **multiple**, which will be an ``int``. The default behavior is to round to increments of 1. """ if not isinstance(multiple, int): raise TypeError("The round multiple must be an int not %s." % multiple.__class__.__name__) self._round(multiple) def _round(self, multiple=1): """ This is the environment implementation of :meth:`BaseKerning.round`. **multiple** will be an ``int``. Subclasses may override this method. """ for pair, value in self.items(): value = int(normalizers.normalizeVisualRounding( value / float(multiple))) * multiple self[pair] = value # ------------- # Interpolation # ------------- def interpolate(self, factor, minKerning, maxKerning, round=True, suppressError=True): r""" Interpolates all pairs between two :class:`BaseKerning` objects: >>> myKerning.interpolate(kerningOne, kerningTwo) **minKerning** and **maxKerning**. The interpolation occurs on a 0 to 1.0 range where **minKerning** is located at 0 and **maxKerning** is located at 1.0. The kerning data is replaced by the interpolated kerning. * **factor** is the interpolation value. It may be less than 0 and greater than 1.0. It may be an :ref:`type-int-float`, ``tuple`` or ``list``. If it is a ``tuple`` or ``list``, the first number indicates the x factor and the second number indicates the y factor. * **round** is a ``bool`` indicating if the result should be rounded to ``int``\s. The default behavior is to round interpolated kerning. * **suppressError** is a ``bool`` indicating if incompatible data should be ignored or if an error should be raised when such incompatibilities are found. The default behavior is to ignore incompatible data. """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minKerning, BaseKerning): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % ( self.__class__.__name__, minKerning.__class__.__name__)) if not isinstance(maxKerning, BaseKerning): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % ( self.__class__.__name__, maxKerning.__class__.__name__)) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate(factor, minKerning, maxKerning, round=round, suppressError=suppressError) def _interpolate(self, factor, minKerning, maxKerning, round=True, suppressError=True): """ This is the environment implementation of :meth:`BaseKerning.interpolate`. * **factor** will be an :ref:`type-int-float`, ``tuple`` or ``list``. * **minKerning** will be a :class:`BaseKerning` object. * **maxKerning** will be a :class:`BaseKerning` object. * **round** will be a ``bool`` indicating if the interpolated kerning should be rounded. * **suppressError** will be a ``bool`` indicating if incompatible data should be ignored. Subclasses may override this method. """ import fontMath from fontMath.mathFunctions import setRoundIntegerFunction setRoundIntegerFunction(normalizers.normalizeVisualRounding) kerningGroupCompatibility = self._testKerningGroupCompatibility( minKerning, maxKerning, suppressError=suppressError ) if not kerningGroupCompatibility: self.clear() else: minKerning = fontMath.MathKerning( kerning=minKerning, groups=minKerning.font.groups) maxKerning = fontMath.MathKerning( kerning=maxKerning, groups=maxKerning.font.groups) result = interpolate(minKerning, maxKerning, factor) if round: result.round() self.clear() result.extractKerning(self.font) @staticmethod def _testKerningGroupCompatibility(minKerning, maxKerning, suppressError=False): minGroups = minKerning.font.groups maxGroups = maxKerning.font.groups match = True while match: for _, sideAttr in ( ("side 1", "side1KerningGroups"), ("side 2", "side2KerningGroups") ): minSideGroups = getattr(minGroups, sideAttr) maxSideGroups = getattr(maxGroups, sideAttr) if minSideGroups.keys() != maxSideGroups.keys(): match = False else: for name in minSideGroups.keys(): minGroup = minSideGroups[name] maxGroup = maxSideGroups[name] if set(minGroup) != set(maxGroup): match = False break if not match and not suppressError: raise ValueError("The kerning groups must be exactly the same.") return match # --------------------- # RoboFab Compatibility # --------------------- def remove(self, pair): r""" Removes a pair from the Kerning. **pair** will be a ``tuple`` of two :ref:`type-string`\s. This is a backwards compatibility method. """ del self[pair] def asDict(self, returnIntegers=True): """ Return the Kerning as a ``dict``. This is a backwards compatibility method. """ d = {} for k, v in self.items(): d[k] = v if not returnIntegers else normalizers.normalizeVisualRounding(v) return d # ------------------- # Inherited Functions # ------------------- def __contains__(self, pair): r""" Tests to see if a pair is in the Kerning. **pair** will be a ``tuple`` of two :ref:`type-string`\s. This returns a ``bool`` indicating if the **pair** is in the Kerning. :: >>> ("A", "V") in font.kerning True """ return super(BaseKerning, self).__contains__(pair) def __delitem__(self, pair): r""" Removes **pair** from the Kerning. **pair** is a ``tuple`` of two :ref:`type-string`\s.:: >>> del font.kerning[("A","V")] """ super(BaseKerning, self).__delitem__(pair) def __getitem__(self, pair): r""" Returns the kerning value of the pair. **pair** is a ``tuple`` of two :ref:`type-string`\s. The returned value will be a :ref:`type-int-float`.:: >>> font.kerning[("A", "V")] -15 It is important to understand that any changes to the returned value will not be reflected in the Kerning object. If one wants to make a change to the value, one should do the following:: >>> value = font.kerning[("A", "V")] >>> value += 10 >>> font.kerning[("A", "V")] = value """ return super(BaseKerning, self).__getitem__(pair) def __iter__(self): """ Iterates through the Kerning, giving the pair for each iteration. The order that the Kerning will iterate though is not fixed nor is it ordered.:: >>> for pair in font.kerning: >>> print pair ("A", "Y") ("A", "V") ("A", "W") """ return super(BaseKerning, self).__iter__() def __len__(self): """ Returns the number of pairs in Kerning as an ``int``.:: >>> len(font.kerning) 5 """ return super(BaseKerning, self).__len__() def __setitem__(self, pair, value): r""" Sets the **pair** to the list of **value**. **pair** is the pair as a ``tuple`` of two :ref:`type-string`\s and **value** is a :ref:`type-int-float`. >>> font.kerning[("A", "V")] = -20 >>> font.kerning[("A", "W")] = -10.5 """ super(BaseKerning, self).__setitem__(pair, value) def clear(self): """ Removes all information from Kerning, resetting the Kerning to an empty dictionary. :: >>> font.kerning.clear() """ super(BaseKerning, self).clear() def get(self, pair, default=None): r""" Returns the value for the kerning pair. **pair** is a ``tuple`` of two :ref:`type-string`\s, and the returned values will either be :ref:`type-int-float` or ``None`` if no pair was found. :: >>> font.kerning[("A", "V")] -25 It is important to understand that any changes to the returned value will not be reflected in the Kerning object. If one wants to make a change to the value, one should do the following:: >>> value = font.kerning[("A", "V")] >>> value += 10 >>> font.kerning[("A", "V")] = value """ return super(BaseKerning, self).get(pair, default) def find(self, pair, default=None): r""" Returns the value for the kerning pair - even if the pair only exists implicitly (one or both sides may be members of a kerning group). **pair** is a ``tuple`` of two :ref:`type-string`\s, and the returned values will either be :ref:`type-int-float` or ``None`` if no pair was found. :: >>> font.kerning[("A", "V")] -25 """ pair = normalizers.normalizeKerningKey(pair) value = self._find(pair, default) if value != default: value = normalizers.normalizeKerningValue(value) return value def _find(self, pair, default=None): """ This is the environment implementation of :attr:`BaseKerning.find`. This must return an :ref:`type-int-float` or `default`. """ from fontTools.ufoLib.kerning import lookupKerningValue font = self.font groups = font.groups return lookupKerningValue(pair, self, groups, fallback=default) def items(self): r""" Returns a list of ``tuple``\s of each pair and value. Pairs are a ``tuple`` of two :ref:`type-string`\s and values are :ref:`type-int-float`. The initial list will be unordered. >>> font.kerning.items() [(("A", "V"), -30), (("A", "W"), -10)] """ return super(BaseKerning, self).items() def keys(self): """ Returns a ``list`` of all the pairs in Kerning. This list will be unordered.:: >>> font.kerning.keys() [("A", "Y"), ("A", "V"), ("A", "W")] """ return super(BaseKerning, self).keys() def pop(self, pair, default=None): r""" Removes the **pair** from the Kerning and returns the value as an ``int``. If no pair is found, **default** is returned. **pair** is a ``tuple`` of two :ref:`type-string`\s. This must return either **default** or a :ref:`type-int-float`. >>> font.kerning.pop(("A", "V")) -20 >>> font.kerning.pop(("A", "W")) -10.5 """ return super(BaseKerning, self).pop(pair, default) def update(self, otherKerning): """ Updates the Kerning based on **otherKerning**. **otherKerning** is a ``dict`` of kerning information. If a pair from **otherKerning** is in Kerning, the pair value will be replaced by the value from **otherKerning**. If a pair from **otherKerning** is not in the Kerning, it is added to the pairs. If Kerning contains a pair that is not in **otherKerning**, it is not changed. >>> font.kerning.update(newKerning) """ super(BaseKerning, self).update(otherKerning) def values(self): r""" Returns a ``list`` of each pair's values, the values will be :ref:`type-int-float`\s. The list will be unordered. >>> font.kerning.items() [-20, -15, 5, 3.5] """ return super(BaseKerning, self).values() robotools-fontParts-26e8b8c/Lib/fontParts/base/layer.py000066400000000000000000000576761477533125200232610ustar00rootroot00000000000000from fontParts.base.base import ( BaseObject, InterpolationMixin, SelectionMixin, dynamicProperty, reference ) from fontParts.base import normalizers from fontParts.base.compatibility import LayerCompatibilityReporter from fontParts.base.color import Color from fontParts.base.deprecated import DeprecatedLayer, RemovedLayer class _BaseGlyphVendor( BaseObject, SelectionMixin, ): """ This class exists to provide common glyph interaction code to BaseFont and BaseLayer. It should not be directly subclassed. """ # ----------------- # Glyph Interaction # ----------------- def _setLayerInGlyph(self, glyph): if glyph.layer is None: if isinstance(self, BaseLayer): layer = self else: layer = self.defaultLayer glyph.layer = layer def __len__(self): """ An ``int`` representing number of glyphs in the layer. :: >>> len(layer) 256 """ return self._len() def _len(self, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__len__` and :meth:`BaseFont.__len__` This must return an ``int`` indicating the number of glyphs in the layer. Subclasses may override this method. """ return len(self.keys()) def __iter__(self): """ Iterate through the :class:`BaseGlyph` objects in the layer. :: >>> for glyph in layer: ... glyph.name "A" "B" "C" """ return self._iter() def _iter(self, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__iter__` and :meth:`BaseFont.__iter__` This must return an iterator that returns instances of a :class:`BaseGlyph` subclass. Subclasses may override this method. """ for name in self.keys(): yield self[name] def __getitem__(self, name): """ Get the :class:`BaseGlyph` with name from the layer. :: >>> glyph = layer["A"] """ name = normalizers.normalizeGlyphName(name) if name not in self: raise KeyError("No glyph named '%s'." % name) glyph = self._getItem(name) self._setLayerInGlyph(glyph) return glyph def _getItem(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__getitem__` and :meth:`BaseFont.__getitem__` This must return an instance of a :class:`BaseGlyph` subclass. **name** will be a :ref:`type-string` representing a name of a glyph that is in the layer. It will have been normalized with :func:`normalizers.normalizeGlyphName`. Subclasses must override this method. """ self.raiseNotImplementedError() def __setitem__(self, name, glyph): """ Insert **glyph** into the layer. :: >>> glyph = layer["A"] = otherGlyph This will not insert the glyph directly. Rather, a new glyph will be created and the data from **glyph** will be copied to the new glyph. **name** indicates the name that should be assigned to the glyph after insertion. If **name** is not given, the glyph's original name must be used. If the glyph does not have a name, an error must be raised. The data that will be inserted from **glyph** is the same data as documented in :meth:`BaseGlyph.copy`. """ name = normalizers.normalizeGlyphName(name) if name in self: del self[name] return self._insertGlyph(glyph, name=name) def __delitem__(self, name): """ Remove the glyph with name from the layer. :: >>> del layer["A"] """ name = normalizers.normalizeGlyphName(name) if name not in self: raise KeyError("No glyph named '%s'." % name) self._removeGlyph(name) def keys(self): """ Get a list of all glyphs in the layer. :: >>> layer.keys() ["B", "C", "A"] The order of the glyphs is undefined. """ return self._keys() def _keys(self, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.keys` and :meth:`BaseFont.keys` This must return an :ref:`type-immutable-list` of the names representing all glyphs in the layer. The order is not defined. Subclasses must override this method. """ self.raiseNotImplementedError() def __contains__(self, name): """ Test if the layer contains a glyph with **name**. :: >>> "A" in layer True """ name = normalizers.normalizeGlyphName(name) return self._contains(name) def _contains(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__contains__` and :meth:`BaseFont.__contains__` This must return ``bool`` indicating if the layer has a glyph with the defined name. **name** will be a :ref-type-string` representing a glyph name. It will have been normalized with :func:`normalizers.normalizeGlyphName`. Subclasses may override this method. """ return name in self.keys() def newGlyph(self, name, clear=True): """ Make a new glyph with **name** in the layer. :: >>> glyph = layer.newGlyph("A") The newly created :class:`BaseGlyph` will be returned. If the glyph exists in the layer and clear is set to ``False``, the existing glyph will be returned, otherwise the default behavior is to clear the exisiting glyph. """ name = normalizers.normalizeGlyphName(name) if name not in self: glyph = self._newGlyph(name) elif clear: self.removeGlyph(name) glyph = self._newGlyph(name) else: glyph = self._getItem(name) self._setLayerInGlyph(glyph) return glyph def _newGlyph(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.newGlyph` and :meth:`BaseFont.newGlyph` This must return an instance of a :class:`BaseGlyph` subclass. **name** will be a :ref:`type-string` representing a glyph name. It will have been normalized with :func:`normalizers.normalizeGlyphName`. The name will have been tested to make sure that no glyph with the same name exists in the layer. Subclasses must override this method. """ self.raiseNotImplementedError() def removeGlyph(self, name): """ Remove the glyph with name from the layer. :: >>> layer.removeGlyph("A") This method is deprecated. :meth:`BaseFont.__delitem__` instead. """ del self[name] def _removeGlyph(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.removeGlyph` and :meth:`BaseFont.removeGlyph`. **name** will be a :ref:`type-string` representing a glyph name of a glyph that is in the layer. It will have been normalized with :func:`normalizers.normalizeGlyphName`. The newly created :class:`BaseGlyph` must be returned. Subclasses must override this method. """ self.raiseNotImplementedError() def insertGlyph(self, glyph, name=None): """ Insert **glyph** into the layer. :: >>> glyph = layer.insertGlyph(otherGlyph, name="A") This method is deprecated. :meth:`BaseFont.__setitem__` instead. """ if name is None: name = glyph.name self[name] = glyph def _insertGlyph(self, glyph, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__setitem__` and :meth:`BaseFont.__setitem__`. This must return an instance of a :class:`BaseGlyph` subclass. **glyph** will be a glyph object with the attributes necessary for copying as defined in :meth:`BaseGlyph.copy` An environment must not insert **glyph** directly. Instead the data from **glyph** should be copied to a new glyph instead. **name** will be a :ref:`type-string` representing a glyph name. It will have been normalized with :func:`normalizers.normalizeGlyphName`. **name** will have been tested to make sure that no glyph with the same name exists in the layer. Subclasses may override this method. """ if glyph.name is None or name != glyph.name: glyph = glyph.copy() glyph.name = name dest = self.newGlyph(name, clear=kwargs.get("clear", True)) dest.copyData(glyph) return dest # --------- # Selection # --------- selectedGlyphs = dynamicProperty( "base_selectedGlyphs", """ A list of glyphs selected in the layer. Getting selected glyph objects: >>> for glyph in layer.selectedGlyphs: ... glyph.markColor = (1, 0, 0, 0.5) Setting selected glyph objects: >>> layer.selectedGlyphs = someGlyphs """ ) def _get_base_selectedGlyphs(self): selected = tuple([normalizers.normalizeGlyph(glyph) for glyph in self._get_selectedGlyphs()]) return selected def _get_selectedGlyphs(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self) def _set_base_selectedGlyphs(self, value): normalized = [normalizers.normalizeGlyph(glyph) for glyph in value] self._set_selectedGlyphs(normalized) def _set_selectedGlyphs(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self, value) selectedGlyphNames = dynamicProperty( "base_selectedGlyphNames", """ A list of names of glyphs selected in the layer. Getting selected glyph names: >>> for name in layer.selectedGlyphNames: ... print(name) Setting selected glyph names: >>> layer.selectedGlyphNames = ["A", "B", "C"] """ ) def _get_base_selectedGlyphNames(self): selected = tuple([normalizers.normalizeGlyphName(name) for name in self._get_selectedGlyphNames()]) return selected def _get_selectedGlyphNames(self): """ Subclasses may override this method. """ selected = [glyph.name for glyph in self.selectedGlyphs] return selected def _set_base_selectedGlyphNames(self, value): normalized = [normalizers.normalizeGlyphName(name) for name in value] self._set_selectedGlyphNames(normalized) def _set_selectedGlyphNames(self, value): """ Subclasses may override this method. """ select = [self[name] for name in value] self.selectedGlyphs = select # -------------------- # Legacy Compatibility # -------------------- has_key = __contains__ class BaseLayer(_BaseGlyphVendor, InterpolationMixin, DeprecatedLayer, RemovedLayer): def _reprContents(self): contents = [ "'%s'" % self.name, ] if self.color: contents.append("color=%r" % str(self.color)) return contents # ---- # Copy # ---- copyAttributes = ( "name", "color", "lib" ) def copy(self): """ Copy the layer into a new layer that does not belong to a font. :: >>> copiedLayer = layer.copy() This will copy: * name * color * lib * glyphs """ return super(BaseLayer, self).copy() def copyData(self, source): """ Copy data from **source** into this layer. Refer to :meth:`BaseLayer.copy` for a list of values that will be copied. """ super(BaseLayer, self).copyData(source) for name in source.keys(): glyph = self.newGlyph(name) glyph.copyData(source[name]) # ------- # Parents # ------- # Font _font = None font = dynamicProperty( "font", """ The layer's parent :class:`BaseFont`. :: >>> font = layer.font """ ) def _get_font(self): if self._font is None: return None return self._font() def _set_font(self, font): if self._font is not None: raise AssertionError("font for layer already set") if font is not None: font = reference(font) self._font = font # -------------- # Identification # -------------- # name name = dynamicProperty( "base_name", """ The name of the layer. :: >>> layer.name "foreground" >>> layer.name = "top" """ ) def _get_base_name(self): value = self._get_name() if value is not None: value = normalizers.normalizeLayerName(value) return value def _set_base_name(self, value): if value == self.name: return value = normalizers.normalizeLayerName(value) font = self.font if font is not None: existing = self.font.layerOrder if value in existing: raise ValueError("A layer with the name '%s' already exists." % value) self._set_name(value) def _get_name(self): """ This is the environment implementation of :attr:`BaseLayer.name`. This must return a :ref:`type-string` defining the name of the layer. If the layer is the default layer, the returned value must be ``None``. It will be normalized with :func:`normalizers.normalizeLayerName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value, **kwargs): """ This is the environment implementation of :attr:`BaseLayer.name`. **value** will be a :ref:`type-string` defining the name of the layer. It will have been normalized with :func:`normalizers.normalizeLayerName`. No layer with the same name will exist. Subclasses must override this method. """ self.raiseNotImplementedError() # color color = dynamicProperty( "base_color", """ The layer's color. :: >>> layer.color None >>> layer.color = (1, 0, 0, 0.5) """ ) def _get_base_color(self): value = self._get_color() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_color(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_color(value) def _get_color(self): """ This is the environment implementation of :attr:`BaseLayer.color`. This must return a :ref:`type-color` defining the color assigned to the layer. If the layer does not have an assigned color, the returned value must be ``None``. It will be normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_color(self, value, **kwargs): """ This is the environment implementation of :attr:`BaseLayer.color`. **value** will be a :ref:`type-color` or ``None`` defining the color to assign to the layer. It will have been normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() # ----------- # Sub-Objects # ----------- # lib lib = dynamicProperty( "base_lib", """ The layer's :class:`BaseLib` object. :: >>> layer.lib["org.robofab.hello"] "world" """ ) def _get_base_lib(self): lib = self._get_lib() lib.font = self return lib def _get_lib(self): """ This is the environment implementation of :attr:`BaseLayer.lib`. This must return an instance of a :class:`BaseLib` subclass. """ self.raiseNotImplementedError() # tempLib tempLib = dynamicProperty( "base_tempLib", """ The layer's :class:`BaseLib` object. :: >>> layer.tempLib["org.robofab.hello"] "world" """ ) def _get_base_tempLib(self): lib = self._get_tempLib() lib.font = self return lib def _get_tempLib(self): """ This is the environment implementation of :attr:`BaseLayer.tempLib`. This must return an instance of a :class:`BaseLib` subclass. """ self.raiseNotImplementedError() # ----------------- # Global Operations # ----------------- def round(self): """ Round all approriate data to integers. :: >>> layer.round() This is the equivalent of calling the round method on: * all glyphs in the layer """ self._round() def _round(self): """ This is the environment implementation of :meth:`BaseLayer.round`. Subclasses may override this method. """ for glyph in self: glyph.round() def autoUnicodes(self): """ Use heuristics to set Unicode values in all glyphs. :: >>> layer.autoUnicodes() Environments will define their own heuristics for automatically determining values. """ self._autoUnicodes() def _autoUnicodes(self): """ This is the environment implementation of :meth:`BaseLayer.autoUnicodes`. Subclasses may override this method. """ for glyph in self: glyph.autoUnicodes() # ------------- # Interpolation # ------------- def interpolate(self, factor, minLayer, maxLayer, round=True, suppressError=True): """ Interpolate all possible data in the layer. :: >>> layer.interpolate(0.5, otherLayer1, otherLayer2) >>> layer.interpolate((0.5, 2.0), otherLayer1, otherLayer2, round=False) The interpolation occurs on a 0 to 1.0 range where **minLayer** is located at 0 and **maxLayer** is located at 1.0. **factor** is the interpolation value. It may be less than 0 and greater than 1.0. It may be a :ref:`type-int-float` or a tuple of two :ref:`type-int-float`. If it is a tuple, the first number indicates the x factor and the second number indicates the y factor. **round** indicates if the result should be rounded to integers. **suppressError** indicates if incompatible data should be ignored or if an error should be raised when such incompatibilities are found. """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minLayer, BaseLayer): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, minLayer.__class__.__name__)) if not isinstance(maxLayer, BaseLayer): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, maxLayer.__class__.__name__)) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate(factor, minLayer, maxLayer, round=round, suppressError=suppressError) def _interpolate(self, factor, minLayer, maxLayer, round=True, suppressError=True): """ This is the environment implementation of :meth:`BaseLayer.interpolate`. Subclasses may override this method. """ for glyphName in self.keys(): del self[glyphName] for glyphName in minLayer.keys(): if glyphName not in maxLayer: continue minGlyph = minLayer[glyphName] maxGlyph = maxLayer[glyphName] dstGlyph = self.newGlyph(glyphName) dstGlyph.interpolate(factor, minGlyph, maxGlyph, round=round, suppressError=suppressError) compatibilityReporterClass = LayerCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compat, report = self.isCompatible(otherLayer) >>> compat False >>> report A - [Fatal] The glyphs do not contain the same number of contours. This will return a ``bool`` indicating if the layer is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseLayer, self).isCompatible(other, BaseLayer) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseLayer.isCompatible`. Subclasses may override this method. """ layer1 = self layer2 = other # incompatible number of glyphs glyphs1 = set(layer1.keys()) glyphs2 = set(layer2.keys()) if len(glyphs1) != len(glyphs2): reporter.glyphCountDifference = True reporter.warning = True if len(glyphs1.difference(glyphs2)) != 0: reporter.warning = True reporter.glyphsMissingFromLayer2 = list(glyphs1.difference(glyphs2)) if len(glyphs2.difference(glyphs1)) != 0: reporter.warning = True reporter.glyphsMissingInLayer1 = list(glyphs2.difference(glyphs1)) # test glyphs for glyphName in sorted(glyphs1.intersection(glyphs2)): glyph1 = layer1[glyphName] glyph2 = layer2[glyphName] glyphCompatibility = glyph1.isCompatible(glyph2)[1] if glyphCompatibility.fatal or glyphCompatibility.warning: if glyphCompatibility.fatal: reporter.fatal = True if glyphCompatibility.warning: reporter.warning = True reporter.glyphs.append(glyphCompatibility) # ------- # mapping # ------- def getReverseComponentMapping(self): """ Create a dictionary of unicode -> [glyphname, ...] mappings. All glyphs are loaded. Note that one glyph can have multiple unicode values, and a unicode value can have multiple glyphs pointing to it. """ return self._getReverseComponentMapping() def _getReverseComponentMapping(self): """ This is the environment implementation of :meth:`BaseFont.getReverseComponentMapping`. Subclasses may override this method. """ self.raiseNotImplementedError() def getCharacterMapping(self): """ Get a reversed map of component references in the font. { 'A' : ['Aacute', 'Aring'] 'acute' : ['Aacute'] 'ring' : ['Aring'] etc. } """ return self._getCharacterMapping() def _getCharacterMapping(self): """ This is the environment implementation of :meth:`BaseFont.getCharacterMapping`. Subclasses may override this method. """ self.raiseNotImplementedError() robotools-fontParts-26e8b8c/Lib/fontParts/base/lib.py000066400000000000000000000207371477533125200226770ustar00rootroot00000000000000from fontParts.base.base import BaseDict, dynamicProperty, reference from fontParts.base import normalizers from fontParts.base.deprecated import DeprecatedLib, RemovedLib class BaseLib(BaseDict, DeprecatedLib, RemovedLib): """ A Lib object. This object normally created as part of a :class:`BaseFont`. An orphan Lib object can be created like this:: >>> lib = RLib() This object behaves like a Python dictionary. Most of the dictionary functionality comes from :class:`BaseDict`, look at that object for the required environment implementation details. Lib uses :func:`normalizers.normalizeLibKey` to normalize the key of the ``dict``, and :func:`normalizers.normalizeLibValue` to normalize the value of the ``dict``. """ keyNormalizer = normalizers.normalizeLibKey valueNormalizer = normalizers.normalizeLibValue def _reprContents(self): contents = [] if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() if self.font: contents.append("in font") contents += self.font._reprContents() return contents # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The lib's parent glyph.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._font is not None: raise AssertionError("font for lib already set") if self._glyph is not None and self._glyph() != glyph: raise AssertionError("glyph for lib already set and is not same as glyph") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Font _font = None font = dynamicProperty("font", "The lib's parent font.") def _get_font(self): if self._font is not None: return self._font() elif self._glyph is not None: return self.glyph.font return None def _set_font(self, font): if self._font is not None and self._font() != font: raise AssertionError("font for lib already set and is not same as font") if self._glyph is not None: raise AssertionError("glyph for lib already set") if font is not None: font = reference(font) self._font = font # Layer layer = dynamicProperty("layer", "The lib's parent layer.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # --------------------- # RoboFab Compatibility # --------------------- def remove(self, key): """ Removes a key from the Lib. **key** will be a :ref:`type-string` that is the key to be removed. This is a backwards compatibility method. """ del self[key] def asDict(self): """ Return the Lib as a ``dict``. This is a backwards compatibility method. """ d = {} for k, v in self.items(): d[k] = v return d # ------------------- # Inherited Functions # ------------------- def __contains__(self, key): """ Tests to see if a lib name is in the Lib. **key** will be a :ref:`type-string`. This returns a ``bool`` indicating if the **key** is in the Lib. :: >>> "public.glyphOrder" in font.lib True """ return super(BaseLib, self).__contains__(key) def __delitem__(self, key): """ Removes **key** from the Lib. **key** is a :ref:`type-string`.:: >>> del font.lib["public.glyphOrder"] """ super(BaseLib, self).__delitem__(key) def __getitem__(self, key): """ Returns the contents of the named lib. **key** is a :ref:`type-string`. The returned value will be a ``list`` of the lib contents.:: >>> font.lib["public.glyphOrder"] ["A", "B", "C"] It is important to understand that any changes to the returned lib contents will not be reflected in the Lib object. If one wants to make a change to the lib contents, one should do the following:: >>> lib = font.lib["public.glyphOrder"] >>> lib.remove("A") >>> font.lib["public.glyphOrder"] = lib """ return super(BaseLib, self).__getitem__(key) def __iter__(self): """ Iterates through the Lib, giving the key for each iteration. The order that the Lib will iterate though is not fixed nor is it ordered.:: >>> for key in font.lib: >>> print key "public.glyphOrder" "org.robofab.scripts.SomeData" "public.postscriptNames" """ return super(BaseLib, self).__iter__() def __len__(self): """ Returns the number of keys in Lib as an ``int``.:: >>> len(font.lib) 5 """ return super(BaseLib, self).__len__() def __setitem__(self, key, items): """ Sets the **key** to the list of **items**. **key** is the lib name as a :ref:`type-string` and **items** is a ``list`` of items as :ref:`type-string`. >>> font.lib["public.glyphOrder"] = ["A", "B", "C"] """ super(BaseLib, self).__setitem__(key, items) def clear(self): """ Removes all keys from Lib, resetting the Lib to an empty dictionary. :: >>> font.lib.clear() """ super(BaseLib, self).clear() def get(self, key, default=None): """ Returns the contents of the named key. **key** is a :ref:`type-string`, and the returned values will either be ``list`` of key contents or ``None`` if no key was found. :: >>> font.lib["public.glyphOrder"] ["A", "B", "C"] It is important to understand that any changes to the returned key contents will not be reflected in the Lib object. If one wants to make a change to the key contents, one should do the following:: >>> lib = font.lib["public.glyphOrder"] >>> lib.remove("A") >>> font.lib["public.glyphOrder"] = lib """ return super(BaseLib, self).get(key, default) def items(self): """ Returns a list of ``tuple`` of each key name and key items. Keys are :ref:`type-string` and key members are a ``list`` of :ref:`type-string`. The initial list will be unordered. >>> font.lib.items() [("public.glyphOrder", ["A", "B", "C"]), ("public.postscriptNames", {'be': 'uni0431', 'ze': 'uni0437'})] """ return super(BaseLib, self).items() def keys(self): """ Returns a ``list`` of all the key names in Lib. This list will be unordered.:: >>> font.lib.keys() ["public.glyphOrder", "org.robofab.scripts.SomeData", "public.postscriptNames"] """ return super(BaseLib, self).keys() def pop(self, key, default=None): """ Removes the **key** from the Lib and returns the ``list`` of key members. If no key is found, **default** is returned. **key** is a :ref:`type-string`. This must return either **default** or a ``list`` of items as :ref:`type-string`. >>> font.lib.pop("public.glyphOrder") ["A", "B", "C"] """ return super(BaseLib, self).pop(key, default) def update(self, otherLib): """ Updates the Lib based on **otherLib**. *otherLib** is a ``dict`` of keys. If a key from **otherLib** is in Lib the key members will be replaced by the key members from **otherLib**. If a key from **otherLib** is not in the Lib, it is added to the Lib. If Lib contain a key name that is not in *otherLib**, it is not changed. >>> font.lib.update(newLib) """ super(BaseLib, self).update(otherLib) def values(self): """ Returns a ``list`` of each named key's members. This will be a list of lists, the key members will be a ``list`` of :ref:`type-string`. The initial list will be unordered. >>> font.lib.items() [["A", "B", "C"], {'be': 'uni0431', 'ze': 'uni0437'}] """ return super(BaseLib, self).values() robotools-fontParts-26e8b8c/Lib/fontParts/base/normalizers.py000066400000000000000000001041311477533125200244650ustar00rootroot00000000000000# -*- coding: utf8 -*- from collections import Counter from fontTools.misc.fixedTools import otRound # ---- # Font # ---- def normalizeFileFormatVersion(value): """ Normalizes a font's file format version. * **value** must be a :ref:`type-int`. * Returned value will be a ``int``. """ if not isinstance(value, int): raise TypeError("File format versions must be instances of " ":ref:`type-int`, not %s." % type(value).__name__) return value def normalizeFileStructure(value): """ Normalizes a font's file structure. * **value** must be a :ref:`type-string`. * Returned value will be a ``string``. """ allowedFileStructures = ["zip", "package"] if value not in allowedFileStructures: raise TypeError("File Structure must be %s, not %s" % (", ".join(allowedFileStructures), value)) return value def normalizeLayerOrder(value, font): """ Normalizes layer order. ** **value** must be a ``tuple`` or ``list``. * **value** items must normalize as layer names with :func:`normalizeLayerName`. * **value** must contain layers that exist in **font**. * **value** must not contain duplicate layers. * Returned ``tuple`` will be unencoded ``unicode`` strings for each layer name. """ if not isinstance(value, (tuple, list)): raise TypeError("Layer order must be a list, not %s." % type(value).__name__) for v in value: normalizeLayerName(v) fontLayers = [layer.name for layer in font.layers] for name in value: if name not in fontLayers: raise ValueError("Layer must exist in font. %s does not exist " "in font.layers." % name) duplicates = [v for v, count in Counter(value).items() if count > 1] if len(duplicates) != 0: raise ValueError("Duplicate layers are not allowed. Layer name(s) " "'%s' are duplicate(s)." % ", ".join(duplicates)) return tuple(value) def normalizeDefaultLayerName(value, font): """ Normalizes default layer name. * **value** must normalize as layer name with :func:`normalizeLayerName`. * **value** must be a layer in **font**. * Returned value will be an unencoded ``unicode`` string. """ value = normalizeLayerName(value) if value not in font.layerOrder: raise ValueError("No layer with the name '%s' exists." % value) return str(value) def normalizeGlyphOrder(value): """ Normalizes glyph order. ** **value** must be a ``tuple`` or ``list``. * **value** items must normalize as glyph names with :func:`normalizeGlyphName`. * **value** must not repeat glyph names. * Returned value will be a ``tuple`` of unencoded ``unicode`` strings. """ if not isinstance(value, (tuple, list)): raise TypeError("Glyph order must be a list, not %s." % type(value).__name__) for v in value: normalizeGlyphName(v) duplicates = sorted(v for v, count in Counter(value).items() if count > 1) if len(duplicates) != 0: raise ValueError("Duplicate glyph names are not allowed. Glyph " "name(s) '%s' are duplicate." % ", ".join(duplicates)) return tuple(value) # ------- # Kerning # ------- def normalizeKerningKey(value): """ Normalizes kerning key. * **value** must be a ``tuple`` or ``list``. * **value** must contain only two members. * **value** items must be :ref:`type-string`. * **value** items must be at least one character long. * Returned value will be a two member ``tuple`` of unencoded ``unicode`` strings. """ if not isinstance(value, (tuple, list)): raise TypeError("Kerning key must be a tuple instance, not %s." % type(value).__name__) if len(value) != 2: raise ValueError("Kerning key must be a tuple containing two items, " "not %d." % len(value)) for v in value: if not isinstance(v, str): raise TypeError("Kerning key items must be strings, not %s." % type(v).__name__) if len(v) < 1: raise ValueError("Kerning key items must be at least one character long") if value[0].startswith("public.") and not value[0].startswith( "public.kern1."): raise ValueError("Left Kerning key group must start with " "public.kern1.") if value[1].startswith("public.") and not value[1].startswith( "public.kern2."): raise ValueError("Right Kerning key group must start with " "public.kern2.") return tuple(value) def normalizeKerningValue(value): """ Normalizes kerning value. * **value** must be an :ref:`type-int-float`. * Returned value is the same type as input value. """ if not isinstance(value, (int, float)): raise TypeError("Kerning value must be a int or a float, not %s." % type(value).__name__) return value # ------ # Groups # ------ def normalizeGroupKey(value): """ Normalizes group key. * **value** must be a :ref:`type-string`. * **value** must be least one character long. * Returned value will be an unencoded ``unicode`` string. """ if not isinstance(value, str): raise TypeError("Group key must be a string, not %s." % type(value).__name__) if len(value) < 1: raise ValueError("Group key must be at least one character long.") return value def normalizeGroupValue(value): """ Normalizes group value. * **value** must be a ``list``. * **value** items must normalize as glyph names with :func:`normalizeGlyphName`. * Returned value will be a ``tuple`` of unencoded ``unicode`` strings. """ if not isinstance(value, (tuple, list)): raise TypeError("Group value must be a list, not %s." % type(value).__name__) value = [normalizeGlyphName(v) for v in value] return tuple(value) # -------- # Features # -------- def normalizeFeatureText(value): """ Normalizes feature text. * **value** must be a :ref:`type-string`. * Returned value will be an unencoded ``unicode`` string. """ if not isinstance(value, str): raise TypeError("Feature text must be a string, not %s." % type(value).__name__) return value # --- # Lib # --- def normalizeLibKey(value): """ Normalizes lib key. * **value** must be a :ref:`type-string`. * **value** must be at least one character long. * Returned value will be an unencoded ``unicode`` string. """ if not isinstance(value, str): raise TypeError("Lib key must be a string, not %s." % type(value).__name__) if len(value) < 1: raise ValueError("Lib key must be at least one character.") return value def normalizeLibValue(value): """ Normalizes lib value. * **value** must not be ``None``. * Returned value is the same type as the input value. """ if value is None: raise ValueError("Lib value must not be None.") if isinstance(value, (list, tuple)): for v in value: normalizeLibValue(v) elif isinstance(value, dict): for k, v in value.items(): normalizeLibKey(k) normalizeLibValue(v) return value # ----- # Layer # ----- def normalizeLayer(value): """ Normalizes layer. * **value** must be a instance of :class:`BaseLayer` * Returned value is the same type as the input value. """ from fontParts.base.layer import BaseLayer return normalizeInternalObjectType(value, BaseLayer, "Layer") def normalizeLayerName(value): """ Normalizes layer name. * **value** must be a :ref:`type-string`. * **value** must be at least one character long. * Returned value will be an unencoded ``unicode`` string. """ if not isinstance(value, str): raise TypeError("Layer names must be strings, not %s." % type(value).__name__) if len(value) < 1: raise ValueError("Layer names must be at least one character long.") return value # ----- # Glyph # ----- def normalizeGlyph(value): """ Normalizes glyph. * **value** must be a instance of :class:`BaseGlyph` * Returned value is the same type as the input value. """ from fontParts.base.glyph import BaseGlyph return normalizeInternalObjectType(value, BaseGlyph, "Glyph") def normalizeGlyphName(value): """ Normalizes glyph name. * **value** must be a :ref:`type-string`. * **value** must be at least one character long. * Returned value will be an unencoded ``unicode`` string. """ if not isinstance(value, str): raise TypeError("Glyph names must be strings, not %s." % type(value).__name__) if len(value) < 1: raise ValueError("Glyph names must be at least one character long.") return value def normalizeGlyphUnicodes(value): """ Normalizes glyph unicodes. * **value** must be a ``list``. * **value** items must normalize as glyph unicodes with :func:`normalizeGlyphUnicode`. * **value** must not repeat unicode values. * Returned value will be a ``tuple`` of ints. """ if not isinstance(value, (tuple, list)): raise TypeError("Glyph unicodes must be a list, not %s." % type(value).__name__) values = [normalizeGlyphUnicode(v) for v in value] duplicates = [v for v, count in Counter(value).items() if count > 1] if len(duplicates) != 0: raise ValueError("Duplicate unicode values are not allowed.") return tuple(values) def normalizeGlyphUnicode(value): """ Normalizes glyph unicode. * **value** must be an int or hex (represented as a string). * **value** must be in a unicode range. * Returned value will be an ``int``. """ if not isinstance(value, (int, str)) or isinstance(value, bool): raise TypeError("Glyph unicode must be a int or hex string, not %s." % type(value).__name__) if isinstance(value, str): try: value = int(value, 16) except ValueError: raise ValueError("Glyph unicode hex must be a valid hex string.") if value < 0 or value > 1114111: raise ValueError("Glyph unicode must be in the Unicode range.") return value def normalizeGlyphWidth(value): """ Normalizes glyph width. * **value** must be a :ref:`type-int-float`. * Returned value is the same type as the input value. """ if not isinstance(value, (int, float)): raise TypeError("Glyph width must be an :ref:`type-int-float`, not %s." % type(value).__name__) return value def normalizeGlyphLeftMargin(value): """ Normalizes glyph left margin. * **value** must be a :ref:`type-int-float` or `None`. * Returned value is the same type as the input value. """ if not isinstance(value, (int, float)) and value is not None: raise TypeError("Glyph left margin must be an :ref:`type-int-float`, " "not %s." % type(value).__name__) return value def normalizeGlyphRightMargin(value): """ Normalizes glyph right margin. * **value** must be a :ref:`type-int-float` or `None`. * Returned value is the same type as the input value. """ if not isinstance(value, (int, float)) and value is not None: raise TypeError("Glyph right margin must be an :ref:`type-int-float`, " "not %s." % type(value).__name__) return value def normalizeGlyphHeight(value): """ Normalizes glyph height. * **value** must be a :ref:`type-int-float`. * Returned value is the same type as the input value. """ if not isinstance(value, (int, float)): raise TypeError("Glyph height must be an :ref:`type-int-float`, not " "%s." % type(value).__name__) return value def normalizeGlyphBottomMargin(value): """ Normalizes glyph bottom margin. * **value** must be a :ref:`type-int-float` or `None`. * Returned value is the same type as the input value. """ if not isinstance(value, (int, float)) and value is not None: raise TypeError("Glyph bottom margin must be an " ":ref:`type-int-float`, not %s." % type(value).__name__) return value def normalizeGlyphTopMargin(value): """ Normalizes glyph top margin. * **value** must be a :ref:`type-int-float` or `None`. * Returned value is the same type as the input value. """ if not isinstance(value, (int, float)) and value is not None: raise TypeError("Glyph top margin must be an :ref:`type-int-float`, " "not %s." % type(value).__name__) return value def normalizeGlyphFormatVersion(value): """ Normalizes glyph format version for saving to XML string. * **value** must be a :ref:`type-int-float` of either 1 or 2. * Returned value will be an int. """ if not isinstance(value, (int, float)): raise TypeError("Glyph Format Version must be an " ":ref:`type-int-float`, not %s." % type(value).__name__) value = int(value) if value not in (1, 2): raise ValueError("Glyph Format Version must be either 1 or 2, not %s." % value) return value # ------- # Contour # ------- def normalizeContour(value): """ Normalizes contour. * **value** must be a instance of :class:`BaseContour` * Returned value is the same type as the input value. """ from fontParts.base.contour import BaseContour return normalizeInternalObjectType(value, BaseContour, "Contour") # ----- # Point # ----- def normalizePointType(value): """ Normalizes point type. * **value** must be an string. * **value** must be one of the following: +----------+ | move | +----------+ | line | +----------+ | offcurve | +----------+ | curve | +----------+ | qcurve | +----------+ * Returned value will be an unencoded ``unicode`` string. """ allowedTypes = ['move', 'line', 'offcurve', 'curve', 'qcurve'] if not isinstance(value, str): raise TypeError("Point type must be a string, not %s." % type(value).__name__) if value not in allowedTypes: raise ValueError("Point type must be '%s'; not %r." % ("', '".join(allowedTypes), value)) return value def normalizePointName(value): """ Normalizes point name. * **value** must be a :ref:`type-string`. * **value** must be at least one character long. * Returned value will be an unencoded ``unicode`` string. """ if not isinstance(value, str): raise TypeError("Point names must be strings, not %s." % type(value).__name__) if len(value) < 1: raise ValueError("Point names must be at least one character long.") return value def normalizePoint(value): """ Normalizes point. * **value** must be a instance of :class:`BasePoint` * Returned value is the same type as the input value. """ from fontParts.base.point import BasePoint return normalizeInternalObjectType(value, BasePoint, "Point") # ------- # Segment # ------- def normalizeSegment(value): """ Normalizes segment. * **value** must be a instance of :class:`BaseSegment` * Returned value is the same type as the input value. """ from fontParts.base.segment import BaseSegment return normalizeInternalObjectType(value, BaseSegment, "Segment") def normalizeSegmentType(value): """ Normalizes segment type. * **value** must be a :ref:`type-string`. * **value** must be one of the following: +--------+ | move | +--------+ | line | +--------+ | curve | +--------+ | qcurve | +--------+ * Returned value will be an unencoded ``unicode`` string. """ allowedTypes = ['move', 'line', 'curve', 'qcurve'] if not isinstance(value, str): raise TypeError("Segment type must be a string, not %s." % type(value).__name__) if value not in allowedTypes: raise ValueError("Segment type must be '%s'; not %r." % ("', '".join(allowedTypes), value)) return value # ------ # BPoint # ------ def normalizeBPoint(value): """ Normalizes bPoint. * **value** must be a instance of :class:`BaseBPoint` * Returned value is the same type as the input value. """ from fontParts.base.bPoint import BaseBPoint return normalizeInternalObjectType(value, BaseBPoint, "bPoint") def normalizeBPointType(value): """ Normalizes bPoint type. * **value** must be an string. * **value** must be one of the following: +--------+ | corner | +--------+ | curve | +--------+ * Returned value will be an unencoded ``unicode`` string. """ allowedTypes = ['corner', 'curve'] if not isinstance(value, str): raise TypeError("bPoint type must be a string, not %s." % type(value).__name__) if value not in allowedTypes: raise ValueError("bPoint type must be 'corner' or 'curve', not %r." % value) return value # --------- # Component # --------- def normalizeComponent(value): """ Normalizes component. * **value** must be a instance of :class:`BaseComponent` * Returned value is the same type as the input value. """ from fontParts.base.component import BaseComponent return normalizeInternalObjectType(value, BaseComponent, "Component") def normalizeComponentScale(value): """ Normalizes component scale. * **value** must be a `tuple`` or ``list``. * **value** must have exactly two items. These items must be instances of :ref:`type-int-float`. * Returned value is a ``tuple`` of two ``float``\s. """ if not isinstance(value, (list, tuple)): raise TypeError("Component scale must be a tuple " "instance, not %s." % type(value).__name__) else: if not len(value) == 2: raise ValueError("Transformation scale tuple must contain two " "values, not %d." % len(value)) for v in value: if not isinstance(v, (int, float)): raise TypeError("Transformation scale tuple values must be an " ":ref:`type-int-float`, not %s." % type(value).__name__) value = tuple([float(v) for v in value]) return value # ------ # Anchor # ------ def normalizeAnchor(value): """ Normalizes anchor. * **value** must be a instance of :class:`BaseAnchor` * Returned value is the same type as the input value. """ from fontParts.base.anchor import BaseAnchor return normalizeInternalObjectType(value, BaseAnchor, "Anchor") def normalizeAnchorName(value): """ Normalizes anchor name. * **value** must be a :ref:`type-string` or ``None``. * **value** must be at least one character long if :ref:`type-string`. * Returned value will be an unencoded ``unicode`` string or ``None``. """ if value is None: return None if not isinstance(value, str): raise TypeError("Anchor names must be strings, not %s." % type(value).__name__) if len(value) < 1: raise ValueError(("Anchor names must be at least one character " "long or None.")) return value # --------- # Guideline # --------- def normalizeGuideline(value): """ Normalizes guideline. * **value** must be a instance of :class:`BaseGuideline` * Returned value is the same type as the input value. """ from fontParts.base.guideline import BaseGuideline return normalizeInternalObjectType(value, BaseGuideline, "Guideline") def normalizeGuidelineName(value): """ Normalizes guideline name. * **value** must be a :ref:`type-string`. * **value** must be at least one character long. * Returned value will be an unencoded ``unicode`` string. """ if not isinstance(value, str): raise TypeError("Guideline names must be strings, not %s." % type(value).__name__) if len(value) < 1: raise ValueError("Guideline names must be at least one character " "long.") return value # ------- # Generic # ------- def normalizeInternalObjectType(value, cls, name): """ Normalizes an internal object type. * **value** must be a instance of **cls**. * Returned value is the same type as the input value. """ if not isinstance(value, cls): raise TypeError("%s must be a %s instance, not %s." % (name, name, type(value).__name__)) return value def normalizeBoolean(value): """ Normalizes a boolean. * **value** must be an ``int`` with value of 0 or 1, or a ``bool``. * Returned value will be a boolean. """ if isinstance(value, int) and value in (0, 1): value = bool(value) if not isinstance(value, bool): raise ValueError("Boolean values must be True or False, not '%s'." % value) return value # Identification def normalizeIndex(value): """ Normalizes index. * **value** must be an ``int`` or ``None``. * Returned value is the same type as the input value. """ if value is not None: if not isinstance(value, int): raise TypeError("Indexes must be None or integers, not %s." % type(value).__name__) return value def normalizeIdentifier(value): """ Normalizes identifier. * **value** must be an :ref:`type-string` or `None`. * **value** must not be longer than 100 characters. * **value** must not contain a character out the range of 0x20 - 0x7E. * Returned value is an unencoded ``unicode`` string. """ if value is None: return value if not isinstance(value, str): raise TypeError("Identifiers must be strings, not %s." % type(value).__name__) if len(value) == 0: raise ValueError("The identifier string is empty.") if len(value) > 100: raise ValueError("The identifier string has a length (%d) greater " "than the maximum allowed (100)." % len(value)) for c in value: v = ord(c) if v < 0x20 or v > 0x7E: raise ValueError("The identifier string ('%s') contains a " "character out size of the range 0x20 - 0x7E." % value) return value # Coordinates def normalizeX(value): """ Normalizes x coordinate. * **value** must be an :ref:`type-int-float`. * Returned value is the same type as the input value. """ if not isinstance(value, (int, float)): raise TypeError("X coordinates must be instances of " ":ref:`type-int-float`, not %s." % type(value).__name__) return value def normalizeY(value): """ Normalizes y coordinate. * **value** must be an :ref:`type-int-float`. * Returned value is the same type as the input value. """ if not isinstance(value, (int, float)): raise TypeError("Y coordinates must be instances of " ":ref:`type-int-float`, not %s." % type(value).__name__) return value def normalizeCoordinateTuple(value): """ Normalizes coordinate tuple. * **value** must be a ``tuple`` or ``list``. * **value** must have exactly two items. * **value** items must be an :ref:`type-int-float`. * Returned value is a ``tuple`` of two values of the same type as the input values. """ if not isinstance(value, (tuple, list)): raise TypeError("Coordinates must be tuple instances, not %s." % type(value).__name__) if len(value) != 2: raise ValueError("Coordinates must be tuples containing two items, " "not %d." % len(value)) x, y = value x = normalizeX(x) y = normalizeY(y) return (x, y) def normalizeBoundingBox(value): """ Normalizes bounding box. * **value** must be an ``tuple`` or ``list``. * **value** must have exactly four items. * **value** items must be :ref:`type-int-float`. * xMin and yMin must be less than or equal to the corresponding xMax, yMax. * Returned value will be a tuple of four ``float``. """ if not isinstance(value, (tuple, list)): raise TypeError("Bounding box be tuple instances, not %s." % type(value).__name__) if len(value) != 4: raise ValueError("Bounding box be tuples containing four items, not " "%d." % len(value)) for v in value: if not isinstance(v, (int, float)): raise TypeError("Bounding box values must be instances of " ":ref:`type-int-float`, not %s." % type(value).__name__) if value[0] > value[2]: raise ValueError("Bounding box xMin must be less than or equal to " "xMax.") if value[1] > value[3]: raise ValueError("Bounding box yMin must be less than or equal to " "yMax.") return tuple([float(v) for v in value]) def normalizeArea(value): """ Normalizes area. * **value** must be a positive :ref:`type-int-float`. """ if not isinstance(value, (int, float)): raise TypeError("Area must be an instance of :ref:`type-int-float`, " "not %s." % type(value).__name__) if value < 0: raise ValueError("Area must be a positive :ref:`type-int-float`, " "not %s." % repr(value)) return float(value) def normalizeRotationAngle(value): """ Normalizes an angle. * Value must be a :ref:`type-int-float`. * Value must be between -360 and 360. * If the value is negative, it is normalized by adding it to 360 * Returned value is a ``float`` between 0 and 360. """ if not isinstance(value, (int, float)): raise TypeError("Angle must be instances of " ":ref:`type-int-float`, not %s." % type(value).__name__) if abs(value) > 360: raise ValueError("Angle must be between -360 and 360.") if value < 0: value = value + 360 return float(value) # Color def normalizeColor(value): """ Normalizes :ref:`type-color`. * **value** must be an ``tuple`` or ``list``. * **value** must have exactly four items. * **value** color components must be between 0 and 1. * Returned value is a ``tuple`` containing four ``float`` values. """ from fontParts.base.color import Color if not isinstance(value, (tuple, list, Color)): raise TypeError("Colors must be tuple instances, not %s." % type(value).__name__) if not len(value) == 4: raise ValueError("Colors must contain four values, not %d." % len(value)) for component, v in zip("rgba", value): if not isinstance(v, (int, float)): raise TypeError("The value for the %s component (%s) is not " "an int or float." % (component, v)) if v < 0 or v > 1: raise ValueError("The value for the %s component (%s) is not " "between 0 and 1." % (component, v)) return tuple([float(v) for v in value]) # Note def normalizeGlyphNote(value): """ Normalizes Glyph Note. * **value** must be a :ref:`type-string`. * Returned value is an unencoded ``unicode`` string """ if not isinstance(value, str): raise TypeError("Note must be a string, not %s." % type(value).__name__) return value # File Path def normalizeFilePath(value): """ Normalizes file path. * **value** must be a :ref:`type-string`. * Returned value is an unencoded ``unicode`` string """ if not isinstance(value, str): raise TypeError("File paths must be strings, not %s." % type(value).__name__) return value # Interpolation def normalizeInterpolationFactor(value): """ Normalizes interpolation factor. * **value** must be an :ref:`type-int-float`, ``tuple`` or ``list``. * If **value** is a ``tuple`` or ``list``, it must have exactly two items. These items must be instances of :ref:`type-int-float`. * Returned value is a ``tuple`` of two ``float``. """ if not isinstance(value, (int, float, list, tuple)): raise TypeError("Interpolation factor must be an int, float, or tuple " "instances, not %s." % type(value).__name__) if isinstance(value, (int, float)): value = (float(value), float(value)) else: if not len(value) == 2: raise ValueError("Interpolation factor tuple must contain two " "values, not %d." % len(value)) for v in value: if not isinstance(v, (int, float)): raise TypeError("Interpolation factor tuple values must be an " ":ref:`type-int-float`, not %s." % type(value).__name__) value = tuple([float(v) for v in value]) return value # --------------- # Transformations # --------------- def normalizeTransformationMatrix(value): """ Normalizes transformation matrix. * **value** must be an ``tuple`` or ``list``. * **value** must have exactly six items. Each of these items must be an instance of :ref:`type-int-float`. * Returned value is a ``tuple`` of six ``float``. """ if not isinstance(value, (tuple, list)): raise TypeError("Transformation matrices must be tuple instances, " "not %s." % type(value).__name__) if not len(value) == 6: raise ValueError("Transformation matrices must contain six values, " "not %d." % len(value)) for v in value: if not isinstance(v, (int, float)): raise TypeError("Transformation matrix values must be instances " "of :ref:`type-int-float`, not %s." % type(v).__name__) return tuple([float(v) for v in value]) def normalizeTransformationOffset(value): """ Normalizes transformation offset. * **value** must be an ``tuple``. * **value** must have exactly two items. Each item must be an instance of :ref:`type-int-float`. * Returned value is a ``tuple`` of two ``float``. """ return normalizeCoordinateTuple(value) def normalizeTransformationSkewAngle(value): """ Normalizes transformation skew angle. * **value** must be an :ref:`type-int-float`, ``tuple`` or ``list``. * If **value** is a ``tuple`` or ``list``, it must have exactly two items. These items must be instances of :ref:`type-int-float`. * **value** items must be between -360 and 360. * If the value is negative, it is normalized by adding it to 360 * Returned value is a ``tuple`` of two ``float`` between 0 and 360. """ if not isinstance(value, (int, float, list, tuple)): raise TypeError("Transformation skew angle must be an int, float, or " "tuple instances, not %s." % type(value).__name__) if isinstance(value, (int, float)): value = (float(value), 0) else: if not len(value) == 2: raise ValueError("Transformation skew angle tuple must contain " "two values, not %d." % len(value)) for v in value: if not isinstance(v, (int, float)): raise TypeError("Transformation skew angle tuple values must " "be an :ref:`type-int-float`, not %s." % type(value).__name__) value = tuple([float(v) for v in value]) for v in value: if abs(v) > 360: raise ValueError("Transformation skew angle must be between -360 " "and 360.") return tuple([float(v + 360) if v < 0 else float(v) for v in value]) def normalizeTransformationScale(value): """ Normalizes transformation scale. * **value** must be an :ref:`type-int-float`, ``tuple`` or ``list``. * If **value** is a ``tuple`` or ``list``, it must have exactly two items. These items must be instances of :ref:`type-int-float`. * Returned value is a ``tuple`` of two ``float``\s. """ if not isinstance(value, (int, float, list, tuple)): raise TypeError("Transformation scale must be an int, float, or tuple " "instances, not %s." % type(value).__name__) if isinstance(value, (int, float)): value = (float(value), float(value)) else: if not len(value) == 2: raise ValueError("Transformation scale tuple must contain two " "values, not %d." % len(value)) for v in value: if not isinstance(v, (int, float)): raise TypeError("Transformation scale tuple values must be an " ":ref:`type-int-float`, not %s." % type(value).__name__) value = tuple([float(v) for v in value]) return value def normalizeVisualRounding(value): """ Normalizes rounding. Python 3 uses banker’s rounding, meaning anything that is at 0.5 will go to the even number. This isn't always ideal for point coordinates, so instead round to the higher number. * **value** must be an :ref:`type-int-float` * Returned value is a ``int`` """ if not isinstance(value, (int, float)): raise TypeError("Value to round must be an int or float, not %s." % type(value).__name__) return otRound(value) robotools-fontParts-26e8b8c/Lib/fontParts/base/point.py000066400000000000000000000247611477533125200232630ustar00rootroot00000000000000from fontTools.misc import transform from fontParts.base.base import ( BaseObject, TransformationMixin, PointPositionMixin, SelectionMixin, IdentifierMixin, dynamicProperty, reference ) from fontParts.base import normalizers from fontParts.base.deprecated import DeprecatedPoint, RemovedPoint class BasePoint( BaseObject, TransformationMixin, PointPositionMixin, SelectionMixin, IdentifierMixin, DeprecatedPoint, RemovedPoint ): """ A point object. This object is almost always created with :meth:`BaseContour.appendPoint`, the pen returned by :meth:`BaseGlyph.getPen` or the point pen returned by :meth:`BaseGLyph.getPointPen`. An orphan point can be created like this:: >>> point = RPoint() """ copyAttributes = ( "type", "smooth", "x", "y", "name" ) def _reprContents(self): contents = [ "%s" % self.type, ("({x}, {y})".format(x=self.x, y=self.y)), ] if self.name is not None: contents.append("name='%s'" % self.name) if self.smooth: contents.append("smooth=%r" % self.smooth) return contents # ------- # Parents # ------- # Contour _contour = None contour = dynamicProperty("contour", "The point's parent :class:`BaseContour`.") def _get_contour(self): if self._contour is None: return None return self._contour() def _set_contour(self, contour): if self._contour is not None: raise AssertionError("contour for point already set") if contour is not None: contour = reference(contour) self._contour = contour # Glyph glyph = dynamicProperty("glyph", "The point's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._contour is None: return None return self.contour.glyph # Layer layer = dynamicProperty("layer", "The point's parent :class:`BaseLayer`.") def _get_layer(self): if self._contour is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The point's parent :class:`BaseFont`.") def _get_font(self): if self._contour is None: return None return self.glyph.font # ---------- # Attributes # ---------- # type type = dynamicProperty( "base_type", """ The point type defined with a :ref:`type-string`. The possible types are: +----------+---------------------------------+ | move | An on-curve move to. | +----------+---------------------------------+ | line | An on-curve line to. | +----------+---------------------------------+ | curve | An on-curve cubic curve to. | +----------+---------------------------------+ | qcurve | An on-curve quadratic curve to. | +----------+---------------------------------+ | offcurve | An off-curve. | +----------+---------------------------------+ """) def _get_base_type(self): value = self._get_type() value = normalizers.normalizePointType(value) return value def _set_base_type(self, value): value = normalizers.normalizePointType(value) self._set_type(value) def _get_type(self): """ This is the environment implementation of :attr:`BasePoint.type`. This must return a :ref:`type-string` defining the point type. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_type(self, value): """ This is the environment implementation of :attr:`BasePoint.type`. **value** will be a :ref:`type-string` defining the point type. It will have been normalized with :func:`normalizers.normalizePointType`. Subclasses must override this method. """ self.raiseNotImplementedError() # smooth smooth = dynamicProperty( "base_smooth", """ A ``bool`` indicating if the point is smooth or not. :: >>> point.smooth False >>> point.smooth = True """ ) def _get_base_smooth(self): value = self._get_smooth() value = normalizers.normalizeBoolean(value) return value def _set_base_smooth(self, value): value = normalizers.normalizeBoolean(value) self._set_smooth(value) def _get_smooth(self): """ This is the environment implementation of :attr:`BasePoint.smooth`. This must return a ``bool`` indicating the smooth state. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_smooth(self, value): """ This is the environment implementation of :attr:`BasePoint.smooth`. **value** will be a ``bool`` indicating the smooth state. It will have been normalized with :func:`normalizers.normalizeBoolean`. Subclasses must override this method. """ self.raiseNotImplementedError() # x x = dynamicProperty( "base_x", """ The x coordinate of the point. It must be an :ref:`type-int-float`. :: >>> point.x 100 >>> point.x = 101 """ ) def _get_base_x(self): value = self._get_x() value = normalizers.normalizeX(value) return value def _set_base_x(self, value): value = normalizers.normalizeX(value) self._set_x(value) def _get_x(self): """ This is the environment implementation of :attr:`BasePoint.x`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_x(self, value): """ This is the environment implementation of :attr:`BasePoint.x`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # y y = dynamicProperty( "base_y", """ The y coordinate of the point. It must be an :ref:`type-int-float`. :: >>> point.y 100 >>> point.y = 101 """ ) def _get_base_y(self): value = self._get_y() value = normalizers.normalizeY(value) return value def _set_base_y(self, value): value = normalizers.normalizeY(value) self._set_y(value) def _get_y(self): """ This is the environment implementation of :attr:`BasePoint.y`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_y(self, value): """ This is the environment implementation of :attr:`BasePoint.y`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Identification # -------------- # index index = dynamicProperty( "base_index", """ The index of the point within the ordered list of the parent glyph's point. This attribute is read only. :: >>> point.index 0 """ ) def _get_base_index(self): value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Get the point's index. This must return an ``int``. Subclasses may override this method. """ contour = self.contour if contour is None: return None return contour.points.index(self) # name name = dynamicProperty( "base_name", """ The name of the point. This will be a :ref:`type-string` or ``None``. >>> point.name 'my point' >>> point.name = None """ ) def _get_base_name(self): value = self._get_name() if value is not None: value = normalizers.normalizePointName(value) return value def _set_base_name(self, value): if value is not None: value = normalizers.normalizePointName(value) self._set_name(value) def _get_name(self): """ This is the environment implementation of :attr:`BasePoint.name`. This must return a :ref:`type-string` or ``None``. The returned value will be normalized with :func:`normalizers.normalizePointName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value): """ This is the environment implementation of :attr:`BasePoint.name`. **value** will be a :ref:`type-string` or ``None``. It will have been normalized with :func:`normalizers.normalizePointName`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ This is the environment implementation of :meth:`BasePoint.transformBy`. **matrix** will be a :ref:`type-transformation`. that has been normalized with :func:`normalizers.normalizeTransformationMatrix`. Subclasses may override this method. """ t = transform.Transform(*matrix) x, y = t.transformPoint((self.x, self.y)) self.x = x self.y = y # ------------- # Normalization # ------------- def round(self): """ Round the point's coordinate. >>> point.round() This applies to the following: * x * y """ self._round() def _round(self, **kwargs): """ This is the environment implementation of :meth:`BasePoint.round`. Subclasses may override this method. """ self.x = normalizers.normalizeVisualRounding(self.x) self.y = normalizers.normalizeVisualRounding(self.y) robotools-fontParts-26e8b8c/Lib/fontParts/base/segment.py000066400000000000000000000242451477533125200235710ustar00rootroot00000000000000from fontParts.base.errors import FontPartsError from fontParts.base.base import ( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, dynamicProperty, reference ) from fontParts.base import normalizers from fontParts.base.deprecated import DeprecatedSegment, RemovedSegment from fontParts.base.compatibility import SegmentCompatibilityReporter class BaseSegment( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, DeprecatedSegment, RemovedSegment ): def _setPoints(self, points): if hasattr(self, "_points"): raise AssertionError("segment has points") self._points = points def _reprContents(self): contents = [ "%s" % self.type, ] if self.index is not None: contents.append("index='%r'" % self.index) return contents # this class should not be used in hashable # collections since it is dynamically generated. __hash__ = None # ------- # Parents # ------- # Contour _contour = None contour = dynamicProperty("contour", "The segment's parent contour.") def _get_contour(self): if self._contour is None: return None return self._contour() def _set_contour(self, contour): if self._contour is not None: raise AssertionError("contour for segment already set") if contour is not None: contour = reference(contour) self._contour = contour # Glyph glyph = dynamicProperty("glyph", "The segment's parent glyph.") def _get_glyph(self): if self._contour is None: return None return self.contour.glyph # Layer layer = dynamicProperty("layer", "The segment's parent layer.") def _get_layer(self): if self._contour is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The segment's parent font.") def _get_font(self): if self._contour is None: return None return self.glyph.font # -------- # Equality # -------- def __eq__(self, other): """ The :meth:`BaseObject.__eq__` method can't be used here because the :class:`BaseContour` implementation contructs segment objects without assigning an underlying ``naked`` object. Therefore, comparisons will always fail. This method overrides the base method and compares the :class:`BasePoint` contained by the segment. Subclasses may override this method. """ if isinstance(other, self.__class__): return self.points == other.points return NotImplemented # -------------- # Identification # -------------- index = dynamicProperty("base_index", ("The index of the segment within the ordered " "list of the parent contour's segments.") ) def _get_base_index(self): if self.contour is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Subclasses may override this method. """ contour = self.contour value = contour.segments.index(self) return value # ---------- # Attributes # ---------- type = dynamicProperty("base_type", ("The segment type. The possible types are " "move, line, curve, qcurve.") ) def _get_base_type(self): value = self._get_type() value = normalizers.normalizeSegmentType(value) return value def _set_base_type(self, value): value = normalizers.normalizeSegmentType(value) self._set_type(value) def _get_type(self): """ Subclasses may override this method. """ onCurve = self.onCurve if onCurve is None: return "qcurve" return onCurve.type def _set_type(self, newType): """ Subclasses may override this method. """ oldType = self.type if oldType == newType: return if self.onCurve is None: # special case with a single qcurve segment # and only offcurves, don't convert return contour = self.contour if contour is None: raise FontPartsError("The segment does not belong to a contour.") # converting line <-> move if newType in ("move", "line") and oldType in ("move", "line"): pass # converting to a move or line elif newType not in ("curve", "qcurve"): offCurves = self.offCurve for point in offCurves: contour.removePoint(point) # converting a line/move to a curve/qcurve else: segments = contour.segments i = segments.index(self) prev = segments[i - 1].onCurve on = self.onCurve x = on.x y = on.y points = contour.points i = points.index(on) contour.insertPoint(i, (x, y), "offcurve") off2 = contour.points[i] contour.insertPoint(i, (prev.x, prev.y), "offcurve") off1 = contour.points[i] del self._points self._setPoints((off1, off2, on)) self.onCurve.type = newType smooth = dynamicProperty("base_smooth", ("Boolean indicating if the segment is " "smooth or not.") ) def _get_base_smooth(self): value = self._get_smooth() value = normalizers.normalizeBoolean(value) return value def _set_base_smooth(self, value): value = normalizers.normalizeBoolean(value) self._set_smooth(value) def _get_smooth(self): """ Subclasses may override this method. """ onCurve = self.onCurve if onCurve is None: return True return onCurve.smooth def _set_smooth(self, value): """ Subclasses may override this method. """ onCurve = self.onCurve if onCurve is not None: self.onCurve.smooth = value # ------ # Points # ------ def __getitem__(self, index): return self._getItem(index) def _getItem(self, index): """ Subclasses may override this method. """ return self.points[index] def __iter__(self): return self._iterPoints() def _iterPoints(self, **kwargs): """ Subclasses may override this method. """ points = self.points count = len(points) index = 0 while count: yield points[index] count -= 1 index += 1 def __len__(self): return self._len() def _len(self, **kwargs): """ Subclasses may override this method. """ return len(self.points) points = dynamicProperty("base_points", "A list of points in the segment.") def _get_base_points(self): return tuple(self._get_points()) def _get_points(self): """ Subclasses may override this method. """ if not hasattr(self, "_points"): return tuple() return tuple(self._points) onCurve = dynamicProperty("base_onCurve", "The on curve point in the segment.") def _get_base_onCurve(self): return self._get_onCurve() def _get_onCurve(self): """ Subclasses may override this method. """ value = self.points[-1] if value.type == "offcurve": return None return value offCurve = dynamicProperty("base_offCurve", "The off curve points in the segment.") def _get_base_offCurve(self): """ Subclasses may override this method. """ return self._get_offCurve() def _get_offCurve(self): """ Subclasses may override this method. """ if self.points and self.points[-1].type == "offcurve": return self.points return self.points[:-1] # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ for point in self.points: point.transformBy(matrix) # ------------- # Interpolation # ------------- compatibilityReporterClass = SegmentCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherSegment) >>> compatible False >>> compatible [Fatal] Segment: [0] + [0] [Fatal] Segment: [0] is line | [0] is move [Fatal] Segment: [1] + [1] [Fatal] Segment: [1] is line | [1] is qcurve This will return a ``bool`` indicating if the segment is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseSegment, self).isCompatible(other, BaseSegment) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseSegment.isCompatible`. Subclasses may override this method. """ segment1 = self segment2 = other # type if segment1.type != segment2.type: # line <-> curve can be converted if set((segment1.type, segment2.type)) != set(("curve", "line")): reporter.typeDifference = True reporter.fatal = True # ---- # Misc # ---- def round(self): """ Round coordinates in all points. """ for point in self.points: point.round() robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/000077500000000000000000000000001477533125200226325ustar00rootroot00000000000000robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/__init__.py000066400000000000000000000014411477533125200247430ustar00rootroot00000000000000from fontParts.base.errors import FontPartsError from fontParts.fontshell.font import RFont from fontParts.fontshell.info import RInfo from fontParts.fontshell.groups import RGroups from fontParts.fontshell.kerning import RKerning from fontParts.fontshell.features import RFeatures from fontParts.fontshell.lib import RLib from fontParts.fontshell.layer import RLayer from fontParts.fontshell.glyph import RGlyph from fontParts.fontshell.contour import RContour from fontParts.fontshell.point import RPoint from fontParts.fontshell.segment import RSegment from fontParts.fontshell.bPoint import RBPoint from fontParts.fontshell.component import RComponent from fontParts.fontshell.anchor import RAnchor from fontParts.fontshell.guideline import RGuideline from fontParts.fontshell.image import RImage robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/anchor.py000066400000000000000000000025411477533125200244600ustar00rootroot00000000000000import defcon from fontParts.base import BaseAnchor from fontParts.fontshell.base import RBaseObject class RAnchor(RBaseObject, BaseAnchor): wrapClass = defcon.Anchor def _init(self, wrap=None): if wrap is None: wrap = self.wrapClass() wrap.x = 0 wrap.y = 0 super(RAnchor, self)._init(wrap=wrap) # -------- # Position # -------- # x def _get_x(self): return self.naked().x def _set_x(self, value): self.naked().x = value # y def _get_y(self): return self.naked().y def _set_y(self, value): self.naked().y = value # -------------- # Identification # -------------- # identifier def _get_identifier(self): anchor = self.naked() return anchor.identifier def _getIdentifier(self): anchor = self.naked() return anchor.generateIdentifier() def _setIdentifier(self, value): self.naked().identifier = value # name def _get_name(self): return self.naked().name def _set_name(self, value): self.naked().name = value # color def _get_color(self): value = self.naked().color if value is not None: value = tuple(value) return value def _set_color(self, value): self.naked().color = value robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/bPoint.py000066400000000000000000000002121477533125200244320ustar00rootroot00000000000000from fontParts.base import BaseBPoint from fontParts.fontshell.base import RBaseObject class RBPoint(BaseBPoint, RBaseObject): pass robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/base.py000066400000000000000000000006271477533125200241230ustar00rootroot00000000000000class RBaseObject(object): wrapClass = None def _init(self, wrap=None): if wrap is None and self.wrapClass is not None: wrap = self.wrapClass() if wrap is not None: self._wrapped = wrap def changed(self): self.naked().dirty = True def naked(self): if hasattr(self, "_wrapped"): return self._wrapped return None robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/component.py000066400000000000000000000026331477533125200252120ustar00rootroot00000000000000import defcon from fontParts.base import BaseComponent from fontParts.fontshell.base import RBaseObject class RComponent(RBaseObject, BaseComponent): wrapClass = defcon.Component # ---------- # Attributes # ---------- # baseGlyph def _get_baseGlyph(self): return self.naked().baseGlyph def _set_baseGlyph(self, value): self.naked().baseGlyph = value # transformation def _get_transformation(self): return self.naked().transformation def _set_transformation(self, value): self.naked().transformation = value # -------------- # Identification # -------------- # index def _set_index(self, value): component = self.naked() glyph = component.glyph if value > glyph.components.index(component): value -= 1 glyph.removeComponent(component) glyph.insertComponent(value, component) # identifier def _get_identifier(self): component = self.naked() return component.identifier def _getIdentifier(self): component = self.naked() return component.generateIdentifier() def _setIdentifier(self, value): self.naked().identifier = value # ------------- # Normalization # ------------- def _decompose(self): component = self.naked() glyph = component.glyph glyph.decomposeComponent(component) robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/contour.py000066400000000000000000000051311477533125200246750ustar00rootroot00000000000000import defcon from fontParts.base import BaseContour from fontParts.fontshell.base import RBaseObject from fontParts.fontshell.point import RPoint from fontParts.fontshell.segment import RSegment from fontParts.fontshell.bPoint import RBPoint class RContour(RBaseObject, BaseContour): wrapClass = defcon.Contour pointClass = RPoint segmentClass = RSegment bPointClass = RBPoint # -------------- # Identification # -------------- # index def _set_index(self, value): contour = self.naked() glyph = contour.glyph glyph.removeContour(contour) glyph.insertContour(value, contour) # identifier def _get_identifier(self): contour = self.naked() return contour.identifier def _getIdentifier(self): contour = self.naked() return contour.generateIdentifier() def _getIdentifierforPoint(self, point): contour = self.naked() point = point.naked() return contour.generateIdentifierForPoint(point) # ---- # Open # ---- def _get_open(self): return self.naked().open # ------ # Bounds # ------ def _get_bounds(self): return self.naked().bounds # ---- # Area # ---- def _get_area(self): return self.naked().area # --------- # Direction # --------- def _get_clockwise(self): return self.naked().clockwise def _reverseContour(self, **kwargs): self.naked().reverse() # ------------------------ # Point and Contour Inside # ------------------------ def _pointInside(self, point): return self.naked().pointInside(point) def _contourInside(self, otherContour): return self.naked().contourInside(otherContour.naked(), segmentLength=5) # ------ # Points # ------ def _lenPoints(self, **kwargs): return len(self.naked()) def _getPoint(self, index, **kwargs): contour = self.naked() point = contour[index] return self.pointClass(point) def _insertPoint(self, index, position, type=None, smooth=None, name=None, identifier=None, **kwargs): point = self.pointClass() point.x = position[0] point.y = position[1] point.type = type point.smooth = smooth point.name = name point = point.naked() point.identifier = identifier self.naked().insertPoint(index, point) def _removePoint(self, index, preserveCurve, **kwargs): contour = self.naked() point = contour[index] contour.removePoint(point) robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/features.py000066400000000000000000000004641477533125200250260ustar00rootroot00000000000000import defcon from fontParts.base import BaseFeatures from fontParts.fontshell.base import RBaseObject class RFeatures(RBaseObject, BaseFeatures): wrapClass = defcon.Features def _get_text(self): return self.naked().text def _set_text(self, value): self.naked().text = value robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/font.py000066400000000000000000000103061477533125200241520ustar00rootroot00000000000000import defcon import os from fontParts.base import BaseFont from fontParts.fontshell.base import RBaseObject from fontParts.fontshell.info import RInfo from fontParts.fontshell.groups import RGroups from fontParts.fontshell.kerning import RKerning from fontParts.fontshell.features import RFeatures from fontParts.fontshell.lib import RLib from fontParts.fontshell.layer import RLayer from fontParts.fontshell.guideline import RGuideline class RFont(RBaseObject, BaseFont): wrapClass = defcon.Font infoClass = RInfo groupsClass = RGroups kerningClass = RKerning featuresClass = RFeatures libClass = RLib layerClass = RLayer guidelineClass = RGuideline # --------------- # File Operations # --------------- # Initialize def _init(self, pathOrObject=None, showInterface=True, **kwargs): if pathOrObject is None: font = self.wrapClass() elif isinstance(pathOrObject, str): font = self.wrapClass(pathOrObject) elif hasattr(pathOrObject, "__fspath__"): font = self.wrapClass(os.fspath(pathOrObject)) else: font = pathOrObject self._wrapped = font # path def _get_path(self, **kwargs): return self.naked().path # save def _save(self, path=None, showProgress=False, formatVersion=None, fileStructure=None, **kwargs): self.naked().save(path=path, formatVersion=formatVersion, structure=fileStructure) # close def _close(self, **kwargs): del self._wrapped # ----------- # Sub-Objects # ----------- # info def _get_info(self): return self.infoClass(wrap=self.naked().info) # groups def _get_groups(self): return self.groupsClass(wrap=self.naked().groups) # kerning def _get_kerning(self): return self.kerningClass(wrap=self.naked().kerning) # features def _get_features(self): return self.featuresClass(wrap=self.naked().features) # lib def _get_lib(self): return self.libClass(wrap=self.naked().lib) # tempLib def _get_tempLib(self): return self.libClass(wrap=self.naked().tempLib) # ------ # Layers # ------ def _get_layers(self, **kwargs): return [self.layerClass(wrap=layer) for layer in self.naked().layers] # order def _get_layerOrder(self, **kwargs): return self.naked().layers.layerOrder def _set_layerOrder(self, value, **kwargs): self.naked().layers.layerOrder = value # default layer def _get_defaultLayerName(self): return self.naked().layers.defaultLayer.name def _set_defaultLayerName(self, value, **kwargs): for layer in self.layers: if layer.name == value: break layer = layer.naked() self.naked().layers.defaultLayer = layer # new def _newLayer(self, name, color, **kwargs): layers = self.naked().layers layer = layers.newLayer(name) layer.color = color return self.layerClass(wrap=layer) # remove def _removeLayer(self, name, **kwargs): layers = self.naked().layers del layers[name] # ------ # Glyphs # ------ def _get_glyphOrder(self): return self.naked().glyphOrder def _set_glyphOrder(self, value): self.naked().glyphOrder = value # ---------- # Guidelines # ---------- def _lenGuidelines(self, **kwargs): return len(self.naked().guidelines) def _getGuideline(self, index, **kwargs): guideline = self.naked().guidelines[index] return self.guidelineClass(guideline) def _appendGuideline(self, position, angle, name=None, color=None, identifier=None, **kwargs): guideline = self.guidelineClass().naked() guideline.x = position[0] guideline.y = position[1] guideline.angle = angle guideline.name = name guideline.color = color guideline.identifier = identifier self.naked().appendGuideline(guideline) return self.guidelineClass(guideline) def _removeGuideline(self, index, **kwargs): guideline = self.naked().guidelines[index] self.naked().removeGuideline(guideline) robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/glyph.py000066400000000000000000000203151477533125200243300ustar00rootroot00000000000000import defcon import booleanOperations from fontParts.base import BaseGlyph from fontParts.base.errors import FontPartsError from fontParts.fontshell.base import RBaseObject from fontParts.fontshell.contour import RContour from fontParts.fontshell.component import RComponent from fontParts.fontshell.anchor import RAnchor from fontParts.fontshell.guideline import RGuideline from fontParts.fontshell.image import RImage from fontParts.fontshell.lib import RLib from fontTools.ufoLib.glifLib import (GlifLibError, readGlyphFromString, writeGlyphToString) class RGlyph(RBaseObject, BaseGlyph): wrapClass = defcon.Glyph contourClass = RContour componentClass = RComponent anchorClass = RAnchor guidelineClass = RGuideline imageClass = RImage libClass = RLib # -------------- # Identification # -------------- # Name def _get_name(self): return self.naked().name def _set_name(self, value): self.naked().name = value # Unicodes def _get_unicodes(self): return self.naked().unicodes def _set_unicodes(self, value): self.naked().unicodes = value # ------- # Metrics # ------- # horizontal def _get_width(self): return self.naked().width def _set_width(self, value): self.naked().width = value def _get_leftMargin(self): return self.naked().leftMargin def _set_leftMargin(self, value): naked = self.naked() naked.leftMargin = value def _get_rightMargin(self): return self.naked().rightMargin def _set_rightMargin(self, value): naked = self.naked() naked.rightMargin = value # vertical def _get_height(self): return self.naked().height def _set_height(self, value): self.naked().height = value def _get_bottomMargin(self): return self.naked().bottomMargin def _set_bottomMargin(self, value): naked = self.naked() naked.bottomMargin = value def _get_topMargin(self): return self.naked().topMargin def _set_topMargin(self, value): naked = self.naked() naked.topMargin = value # ------ # Bounds # ------ def _get_bounds(self): return self.naked().bounds # ---- # Area # ---- def _get_area(self): return self.naked().area # ---- # Pens # ---- def getPen(self): return self.naked().getPen() def getPointPen(self): return self.naked().getPointPen() # ----------------------------------------- # Contour, Component and Anchor Interaction # ----------------------------------------- # Contours def _lenContours(self, **kwargs): return len(self.naked()) def _getContour(self, index, **kwargs): glyph = self.naked() contour = glyph[index] return self.contourClass(contour) def _removeContour(self, index, **kwargs): glyph = self.naked() contour = glyph[index] glyph.removeContour(contour) def _removeOverlap(self, **kwargs): if len(self): contours = list(self) for contour in contours: for point in contour.points: if point.type == "qcurve": raise TypeError("fontshell can't removeOverlap for quadratics") self.clear(contours=True, components=False, anchors=False, guidelines=False, image=False) booleanOperations.union(contours, self.getPointPen()) def _correctDirection(self, trueType=False, **kwargs): self.naked().correctContourDirection(trueType=trueType) # Components def _lenComponents(self, **kwargs): return len(self.naked().components) def _getComponent(self, index, **kwargs): glyph = self.naked() component = glyph.components[index] return self.componentClass(component) def _removeComponent(self, index, **kwargs): glyph = self.naked() component = glyph.components[index] glyph.removeComponent(component) # Anchors def _lenAnchors(self, **kwargs): return len(self.naked().anchors) def _getAnchor(self, index, **kwargs): glyph = self.naked() anchor = glyph.anchors[index] return self.anchorClass(anchor) def _appendAnchor(self, name, position=None, color=None, identifier=None, **kwargs): glyph = self.naked() anchor = self.anchorClass().naked() anchor.name = name anchor.x = position[0] anchor.y = position[1] anchor.color = color anchor.identifier = identifier glyph.appendAnchor(anchor) wrapped = self.anchorClass(anchor) wrapped.glyph = self return wrapped def _removeAnchor(self, index, **kwargs): glyph = self.naked() anchor = glyph.anchors[index] glyph.removeAnchor(anchor) # Guidelines def _lenGuidelines(self, **kwargs): return len(self.naked().guidelines) def _getGuideline(self, index, **kwargs): glyph = self.naked() guideline = glyph.guidelines[index] return self.guidelineClass(guideline) def _appendGuideline(self, position, angle, name=None, color=None, identifier=None, **kwargs): glyph = self.naked() guideline = self.guidelineClass().naked() guideline.x = position[0] guideline.y = position[1] guideline.angle = angle guideline.name = name guideline.color = color guideline.identifier = identifier glyph.appendGuideline(guideline) return self.guidelineClass(guideline) def _removeGuideline(self, index, **kwargs): glyph = self.naked() guideline = glyph.guidelines[index] glyph.removeGuideline(guideline) # ----------------- # Layer Interaction # ----------------- # new def _newLayer(self, name, **kwargs): layerName = name glyphName = self.name font = self.font if layerName not in font.layerOrder: layer = font.newLayer(layerName) else: layer = font.getLayer(layerName) glyph = layer.newGlyph(glyphName) return glyph # remove def _removeLayer(self, name, **kwargs): layerName = name glyphName = self.name font = self.font layer = font.getLayer(layerName) layer.removeGlyph(glyphName) # ----- # Image # ----- def _get_image(self): image = self.naked().image if image is None: return None return self.imageClass(image) def _addImage(self, data, transformation=None, color=None): image = self.naked().image image = self.imageClass(image) image.glyph = self image.data = data image.transformation = transformation image.color = color def _clearImage(self, **kwargs): self.naked().image = None # ---- # Note # ---- # Mark def _get_markColor(self): value = self.naked().markColor if value is not None: value = tuple(value) return value def _set_markColor(self, value): self.naked().markColor = value # Note def _get_note(self): return self.naked().note def _set_note(self, value): self.naked().note = value # ----------- # Sub-Objects # ----------- # lib def _get_lib(self): return self.libClass(wrap=self.naked().lib) # tempLib def _get_tempLib(self): return self.libClass(wrap=self.naked().tempLib) # --- # API # --- def _loadFromGLIF(self, glifData, validate=True): try: readGlyphFromString( aString=glifData, glyphObject=self.naked(), pointPen=self.getPointPen(), validate=validate ) except GlifLibError: raise FontPartsError("Not valid glif data") def _dumpToGLIF(self, glyphFormatVersion): glyph = self.naked() return writeGlyphToString( glyphName=glyph.name, glyphObject=glyph, drawPointsFunc=glyph.drawPoints, formatVersion=glyphFormatVersion ) robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/groups.py000066400000000000000000000013471477533125200245300ustar00rootroot00000000000000import defcon from fontParts.base import BaseGroups from fontParts.fontshell.base import RBaseObject class RGroups(RBaseObject, BaseGroups): wrapClass = defcon.Groups def _get_side1KerningGroups(self): return self.naked().getRepresentation("defcon.groups.kerningSide1Groups") def _get_side2KerningGroups(self): return self.naked().getRepresentation("defcon.groups.kerningSide2Groups") def _items(self): return self.naked().items() def _contains(self, key): return key in self.naked() def _setItem(self, key, value): self.naked()[key] = list(value) def _getItem(self, key): return self.naked()[key] def _delItem(self, key): del self.naked()[key] robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/guideline.py000066400000000000000000000030461477533125200251540ustar00rootroot00000000000000import defcon from fontParts.base import BaseGuideline from fontParts.fontshell.base import RBaseObject class RGuideline(RBaseObject, BaseGuideline): wrapClass = defcon.Guideline def _init(self, wrap=None): if wrap is None: wrap = self.wrapClass() wrap.x = 0 wrap.y = 0 wrap.angle = 0 super(RGuideline, self)._init(wrap=wrap) # -------- # Position # -------- # x def _get_x(self): return self.naked().x def _set_x(self, value): self.naked().x = value # y def _get_y(self): return self.naked().y def _set_y(self, value): self.naked().y = value # angle def _get_angle(self): return self.naked().angle def _set_angle(self, value): self.naked().angle = value # -------------- # Identification # -------------- # identifier def _get_identifier(self): guideline = self.naked() return guideline.identifier def _getIdentifier(self): guideline = self.naked() return guideline.generateIdentifier() def _setIdentifier(self, value): self.naked().identifier = value # name def _get_name(self): return self.naked().name def _set_name(self, value): self.naked().name = value # color def _get_color(self): value = self.naked().color if value is not None: value = tuple(value) return value def _set_color(self, value): self.naked().color = value robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/image.py000066400000000000000000000035661477533125200243000ustar00rootroot00000000000000import defcon from fontParts.base import BaseImage, FontPartsError from fontParts.fontshell.base import RBaseObject class RImage(RBaseObject, BaseImage): wrapClass = defcon.Image _orphanData = None _orphanColor = None # ---------- # Attributes # ---------- # Transformation def _get_transformation(self): return self.naked().transformation def _set_transformation(self, value): self.naked().transformation = value # Color def _get_color(self): if self.font is None: return self._orphanColor value = self.naked().color if value is not None: value = tuple(value) return value def _set_color(self, value): if self.font is None: self._orphanColor = value else: self.naked().color = value # Data def _get_data(self): if self.font is None: return self._orphanData image = self.naked() images = self.font.naked().images fileName = image.fileName if fileName is None: return None if fileName not in images: return None return images[fileName] def _set_data(self, value): from fontTools.ufoLib.validators import pngValidator if not isinstance(value, bytes): raise FontPartsError("The image data provided is not valid.") if not pngValidator(data=value)[0]: raise FontPartsError("The image must be in PNG format.") if self.font is None: self._orphanData = value else: image = self.naked() images = image.font.images fileName = images.findDuplicateImage(value) if fileName is None: fileName = images.makeFileName("image.png") images[fileName] = value image.fileName = fileName robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/info.py000066400000000000000000000005011477533125200241330ustar00rootroot00000000000000import defcon from fontParts.base import BaseInfo from fontParts.fontshell.base import RBaseObject class RInfo(RBaseObject, BaseInfo): wrapClass = defcon.Info def _getAttr(self, attr): return getattr(self.naked(), attr) def _setAttr(self, attr, value): setattr(self.naked(), attr, value) robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/kerning.py000066400000000000000000000011101477533125200246320ustar00rootroot00000000000000import defcon from fontParts.base import BaseKerning from fontParts.fontshell.base import RBaseObject class RKerning(RBaseObject, BaseKerning): wrapClass = defcon.Kerning def _items(self): return self.naked().items() def _contains(self, key): return key in self.naked() def _setItem(self, key, value): self.naked()[key] = value def _getItem(self, key): return self.naked()[key] def _delItem(self, key): del self.naked()[key] def _find(self, pair, default=0): return self.naked().find(pair, default) robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/layer.py000066400000000000000000000032511477533125200243210ustar00rootroot00000000000000import defcon from fontParts.base import BaseLayer from fontParts.fontshell.base import RBaseObject from fontParts.fontshell.lib import RLib from fontParts.fontshell.glyph import RGlyph class RLayer(RBaseObject, BaseLayer): wrapClass = defcon.Layer libClass = RLib glyphClass = RGlyph # ----------- # Sub-Objects # ----------- # lib def _get_lib(self): return self.libClass(wrap=self.naked().lib) # tempLib def _get_tempLib(self): return self.libClass(wrap=self.naked().tempLib) # -------------- # Identification # -------------- # name def _get_name(self): return self.naked().name def _set_name(self, value, **kwargs): self.naked().name = value # color def _get_color(self): value = self.naked().color if value is not None: value = tuple(value) return value def _set_color(self, value, **kwargs): self.naked().color = value # ----------------- # Glyph Interaction # ----------------- def _getItem(self, name, **kwargs): layer = self.naked() glyph = layer[name] return self.glyphClass(glyph) def _keys(self, **kwargs): return self.naked().keys() def _newGlyph(self, name, **kwargs): layer = self.naked() layer.newGlyph(name) return self[name] def _removeGlyph(self, name, **kwargs): layer = self.naked() del layer[name] # ------- # mapping # ------- def _getReverseComponentMapping(self): return self.naked().componentReferences def _getCharacterMapping(self): return self.naked().unicodeData robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/lib.py000066400000000000000000000007411477533125200237540ustar00rootroot00000000000000import defcon from fontParts.base import BaseLib from fontParts.fontshell.base import RBaseObject class RLib(RBaseObject, BaseLib): wrapClass = defcon.Lib def _items(self): return self.naked().items() def _contains(self, key): return key in self.naked() def _setItem(self, key, value): self.naked()[key] = value def _getItem(self, key): return self.naked()[key] def _delItem(self, key): del self.naked()[key] robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/point.py000066400000000000000000000045531477533125200243440ustar00rootroot00000000000000import defcon from fontParts.base import BasePoint, FontPartsError from fontParts.fontshell.base import RBaseObject class RPoint(RBaseObject, BasePoint): wrapClass = defcon.Point def _init(self, wrap=None): if wrap is None: wrap = self.wrapClass((0, 0)) super(RPoint, self)._init(wrap=wrap) def _postChangeNotification(self): contour = self.contour if contour is None: return contour.naked().postNotification("Contour.PointsChanged") self.changed() def changed(self): self.contour.naked().dirty = True # ---------- # Attributes # ---------- # type def _get_type(self): value = self.naked().segmentType if value is None: value = "offcurve" return value def _set_type(self, value): if value == "offcurve": value = None self.naked().segmentType = value self._postChangeNotification() # smooth def _get_smooth(self): return self.naked().smooth def _set_smooth(self, value): self.naked().smooth = value self._postChangeNotification() # x def _get_x(self): return self.naked().x def _set_x(self, value): self.naked().x = value self._postChangeNotification() # y def _get_y(self): return self.naked().y def _set_y(self, value): self.naked().y = value self._postChangeNotification() # -------------- # Identification # -------------- # name def _get_name(self): return self.naked().name def _set_name(self, value): self.naked().name = value self._postChangeNotification() # identifier def _get_identifier(self): point = self.naked() return point.identifier def _getIdentifier(self): point = self.naked() value = point.identifier if value is not None: return value if self.contour is not None: contour = self.contour.naked() contour.generateIdentifierForPoint(point) value = point.identifier else: raise FontPartsError(("An identifier can not be generated " "for this point because it does not " "belong to a contour.")) return value robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/segment.py000066400000000000000000000002151477533125200246440ustar00rootroot00000000000000from fontParts.base import BaseSegment from fontParts.fontshell.base import RBaseObject class RSegment(BaseSegment, RBaseObject): pass robotools-fontParts-26e8b8c/Lib/fontParts/fontshell/test.py000066400000000000000000000100371477533125200241640ustar00rootroot00000000000000from fontParts.test import testEnvironment from fontParts.fontshell.font import RFont from fontParts.fontshell.info import RInfo from fontParts.fontshell.groups import RGroups from fontParts.fontshell.kerning import RKerning from fontParts.fontshell.features import RFeatures from fontParts.fontshell.layer import RLayer from fontParts.fontshell.glyph import RGlyph from fontParts.fontshell.contour import RContour from fontParts.fontshell.segment import RSegment from fontParts.fontshell.bPoint import RBPoint from fontParts.fontshell.point import RPoint from fontParts.fontshell.anchor import RAnchor from fontParts.fontshell.component import RComponent from fontParts.fontshell.image import RImage from fontParts.fontshell.lib import RLib from fontParts.fontshell.guideline import RGuideline # defcon does not have prebuilt support for # selection states, so we simulate selection # behavior with a small subclasses for testing # purposes only. def _get_selected(self): if isinstance(self, FSTestSegment): for point in self.points: if point.selected: return True return False elif isinstance(self, FSTestBPoint): point = self._point.naked() return point.name == "selected" elif isinstance(self, FSTestPoint): return self.name == "selected" else: if not hasattr(self.naked(), "_testSelected"): return False return self.naked()._testSelected def _set_selected(self, value): if isinstance(self, FSTestSegment): for point in self.points: point.selected = value elif isinstance(self, FSTestBPoint): point = self._point.naked() if value: point.name = "selected" else: point.name = None elif isinstance(self, FSTestPoint): if value: self.name = "selected" else: self.name = None else: self.naked()._testSelected = value class FSTestPoint(RPoint): _get_selected = _get_selected _set_selected = _set_selected class FSTestBPoint(RBPoint): _get_selected = _get_selected _set_selected = _set_selected class FSTestSegment(RSegment): _get_selected = _get_selected _set_selected = _set_selected class FSTestGuideline(RGuideline): _get_selected = _get_selected _set_selected = _set_selected class FSTestImage(RImage): _get_selected = _get_selected _set_selected = _set_selected class FSTestAnchor(RAnchor): _get_selected = _get_selected _set_selected = _set_selected class FSTestComponent(RComponent): _get_selected = _get_selected _set_selected = _set_selected class FSTestContour(RContour): segmentClass = FSTestSegment bPointClass = FSTestBPoint pointClass = FSTestPoint _get_selected = _get_selected _set_selected = _set_selected class FSTestGlyph(RGlyph): contourClass = FSTestContour componentClass = FSTestComponent anchorClass = FSTestAnchor guidelineClass = FSTestGuideline _get_selected = _get_selected _set_selected = _set_selected class FSTestLayer(RLayer): glyphClass = FSTestGlyph _get_selected = _get_selected _set_selected = _set_selected class FSTestFont(RFont): layerClass = FSTestLayer guidelineClass = FSTestGuideline _get_selected = _get_selected _set_selected = _set_selected classMapping = dict( font=FSTestFont, info=RInfo, groups=RGroups, kerning=RKerning, features=RFeatures, layer=FSTestLayer, glyph=FSTestGlyph, contour=FSTestContour, segment=FSTestSegment, bPoint=FSTestBPoint, point=FSTestPoint, anchor=FSTestAnchor, component=FSTestComponent, image=FSTestImage, lib=RLib, guideline=FSTestGuideline, ) def fontshellObjectGenerator(cls): unrequested = [] obj = classMapping[cls]() return obj, unrequested if __name__ == "__main__": import sys if {"-v", "--verbose"}.intersection(sys.argv): verbosity = 2 else: verbosity = 1 testEnvironment(fontshellObjectGenerator, verbosity=verbosity) robotools-fontParts-26e8b8c/Lib/fontParts/test/000077500000000000000000000000001477533125200216135ustar00rootroot00000000000000robotools-fontParts-26e8b8c/Lib/fontParts/test/__init__.py000066400000000000000000000040741477533125200237310ustar00rootroot00000000000000from __future__ import print_function import sys import unittest from fontParts.test import test_normalizers from fontParts.test import test_font from fontParts.test import test_info from fontParts.test import test_groups from fontParts.test import test_kerning from fontParts.test import test_features from fontParts.test import test_layer from fontParts.test import test_glyph from fontParts.test import test_contour from fontParts.test import test_segment from fontParts.test import test_bPoint from fontParts.test import test_point from fontParts.test import test_component from fontParts.test import test_anchor from fontParts.test import test_image from fontParts.test import test_lib from fontParts.test import test_guideline from fontParts.test import test_deprecated from fontParts.test import test_color from fontParts.test import test_world def testEnvironment(objectGenerator, inApp=False, verbosity=1, testNormalizers=True): modules = [ test_font, test_info, test_groups, test_kerning, test_features, test_layer, test_glyph, test_contour, test_segment, test_bPoint, test_point, test_component, test_anchor, test_image, test_lib, test_guideline, test_deprecated, test_color, test_world ] if testNormalizers: modules.append(test_normalizers) globalSuite = unittest.TestSuite() loader = unittest.TestLoader() for module in modules: suite = loader.loadTestsFromModule(module) _setObjectGenerator(suite, objectGenerator) globalSuite.addTest(suite) runner = unittest.TextTestRunner(verbosity=verbosity) succes = runner.run(globalSuite).wasSuccessful() if not inApp: sys.exit(not succes) else: return succes # pragma: no cover def _setObjectGenerator(suite, objectGenerator): for i in suite: if isinstance(i, unittest.TestSuite): _setObjectGenerator(i, objectGenerator) else: i.objectGenerator = objectGenerator robotools-fontParts-26e8b8c/Lib/fontParts/test/legacyPointPen.py000066400000000000000000000013541477533125200251110ustar00rootroot00000000000000from fontPens.recordingPointPen import RecordingPointPen class LegacyPointPen(RecordingPointPen): """ A point pen that accepts only the original arguments in the various methods. """ def beginPath(self): super(LegacyPointPen, self).beginPath() def endPath(self): super(LegacyPointPen, self).endPath() def addPoint(self, pt, segmentType=None, smooth=False, name=None): super(LegacyPointPen, self).addPoint(pt, segmentType=segmentType, smooth=smooth, name=name) def addComponent(self, baseGlyphName, transformation): super(LegacyPointPen, self).addComponent(baseGlyphName, transformation) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_anchor.py000066400000000000000000000463731477533125200245130ustar00rootroot00000000000000import unittest import collections from fontParts.base import FontPartsError class TestAnchor(unittest.TestCase): def getAnchor_generic(self): anchor, _ = self.objectGenerator("anchor") anchor.name = "Anchor Attribute Test" anchor.x = 1 anchor.y = 2 anchor.color = None return anchor # ---- # repr # ---- def test_reprContents(self): anchor = self.getAnchor_generic() value = anchor._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_noGlyph(self): anchor, _ = self.objectGenerator("anchor") value = anchor._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_color(self): anchor = self.getAnchor_generic() anchor.color = (1, 0, 1, 1) value = anchor._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_noGlyph_color(self): anchor, _ = self.objectGenerator("anchor") anchor.color = (1, 0, 1, 1) value = anchor._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) # ---------- # Attributes # ---------- # Name def test_get(self): anchor = self.getAnchor_generic() self.assertEqual(anchor.name, "Anchor Attribute Test") def test_set_valid(self): anchor = self.getAnchor_generic() anchor.name = u"foo" self.assertEqual(anchor.name, u"foo") def test_set_none(self): anchor = self.getAnchor_generic() anchor.name = None self.assertIsNone(anchor.name) def test_set_invalid(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.name = 123 # Color def test_color_get_none(self): anchor = self.getAnchor_generic() self.assertIsNone(anchor.color) def test_color_set_valid_max(self): anchor = self.getAnchor_generic() anchor.color = (1, 1, 1, 1) self.assertEqual(anchor.color, (1, 1, 1, 1)) def test_color_set_valid_min(self): anchor = self.getAnchor_generic() anchor.color = (0, 0, 0, 0) self.assertEqual(anchor.color, (0, 0, 0, 0)) def test_color_set_valid_decimal(self): anchor = self.getAnchor_generic() anchor.color = (0.1, 0.2, 0.3, 0.4) self.assertEqual(anchor.color, (0.1, 0.2, 0.3, 0.4)) def test_color_set_none(self): anchor = self.getAnchor_generic() anchor.color = None self.assertIsNone(anchor.color) def test_color_set_invalid_over_max(self): anchor = self.getAnchor_generic() with self.assertRaises(ValueError): anchor.color = (1.1, 0.2, 0.3, 0.4) def test_color_set_invalid_uner_min(self): anchor = self.getAnchor_generic() with self.assertRaises(ValueError): anchor.color = (-0.1, 0.2, 0.3, 0.4) def test_color_set_invalid_too_few(self): anchor = self.getAnchor_generic() with self.assertRaises(ValueError): anchor.color = (0.1, 0.2, 0.3) def test_color_set_invalid_string(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.color = "0.1,0.2,0.3,0.4" def test_color_set_invalid_int(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.color = 123 # Identifier def test_identifier_get_none(self): anchor = self.getAnchor_generic() self.assertIsNone(anchor.identifier) def test_identifier_generated_type(self): anchor = self.getAnchor_generic() anchor.getIdentifier() self.assertIsInstance(anchor.identifier, str) def test_identifier_consistency(self): anchor = self.getAnchor_generic() anchor.getIdentifier() # get: twice to test consistency self.assertEqual(anchor.identifier, anchor.identifier) def test_identifier_cannot_set(self): # identifier is a read-only property anchor = self.getAnchor_generic() with self.assertRaises(FontPartsError): anchor.identifier = "ABC" def test_identifier_force_set(self): identifier = "ABC" anchor = self.getAnchor_generic() anchor._setIdentifier(identifier) self.assertEqual(anchor.identifier, identifier) # Index def getAnchor_index(self): glyph, _ = self.objectGenerator("glyph") glyph.appendAnchor("anchor 0", (0, 0)) glyph.appendAnchor("anchor 1", (0, 0)) glyph.appendAnchor("anchor 2", (0, 0)) return glyph def test_get_index_noParent(self): anchor, _ = self.objectGenerator("anchor") self.assertIsNone(anchor.index) def test_get_index(self): glyph = self.getAnchor_index() for i, anchor in enumerate(glyph.anchors): self.assertEqual(anchor.index, i) def test_set_index_noParent(self): anchor, _ = self.objectGenerator("anchor") with self.assertRaises(FontPartsError): anchor.index = 1 def test_set_index_positive(self): glyph = self.getAnchor_index() anchor = glyph.anchors[0] with self.assertRaises(FontPartsError): anchor.index = 2 def test_set_index_negative(self): glyph = self.getAnchor_index() anchor = glyph.anchors[1] with self.assertRaises(FontPartsError): anchor.index = -1 # x def test_x_get(self): anchor = self.getAnchor_generic() self.assertEqual(anchor.x, 1) def test_x_set_valid_positive(self): anchor = self.getAnchor_generic() anchor.x = 100 self.assertEqual(anchor.x, 100) def test_x_set_valid_negative(self): anchor = self.getAnchor_generic() anchor.x = -100 self.assertEqual(anchor.x, -100) def test_x_set_valid_zero(self): anchor = self.getAnchor_generic() anchor.x = 0 self.assertEqual(anchor.x, 0) def test_x_set_valid_positive_decimal(self): anchor = self.getAnchor_generic() anchor.x = 1.1 self.assertEqual(anchor.x, 1.1) def test_x_set_valid_negative_decimal(self): anchor = self.getAnchor_generic() anchor.x = -1.1 self.assertEqual(anchor.x, -1.1) def test_x_set_invalid_none(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.x = None def test_x_set_valid_string(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.x = "ABC" # y def test_y_get(self): anchor = self.getAnchor_generic() self.assertEqual(anchor.y, 2) def test_y_set_valid_positive(self): anchor = self.getAnchor_generic() anchor.y = 100 self.assertEqual(anchor.y, 100) def test_y_set_valid_negative(self): anchor = self.getAnchor_generic() anchor.y = -100 self.assertEqual(anchor.y, -100) def test_y_set_valid_zero(self): anchor = self.getAnchor_generic() anchor.y = 0 self.assertEqual(anchor.y, 0) def test_y_set_valid_positive_decimal(self): anchor = self.getAnchor_generic() anchor.y = 1.1 self.assertEqual(anchor.y, 1.1) def test_y_set_valid_negative_decimal(self): anchor = self.getAnchor_generic() anchor.y = -1.1 self.assertEqual(anchor.y, -1.1) def test_y_set_invalid_none(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.y = None def test_y_set_valid_string(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.y = "ABC" # ------- # Methods # ------- def getAnchor_copy(self): anchor = self.getAnchor_generic() anchor.color = (0.1, 0.2, 0.3, 0.4) return anchor # copy def test_copy_seperate_objects(self): anchor = self.getAnchor_copy() copied = anchor.copy() self.assertIsNot(anchor, copied) def test_copy_same_name(self): anchor = self.getAnchor_copy() copied = anchor.copy() self.assertEqual(anchor.name, copied.name) def test_copy_same_color(self): anchor = self.getAnchor_copy() copied = anchor.copy() self.assertEqual(anchor.color, copied.color) def test_copy_same_identifier(self): anchor = self.getAnchor_copy() copied = anchor.copy() self.assertEqual(anchor.identifier, copied.identifier) def test_copy_generated_identifier_different(self): anchor = self.getAnchor_copy() copied = anchor.copy() anchor.getIdentifier() copied.getIdentifier() self.assertNotEqual(anchor.identifier, copied.identifier) def test_copy_same_x(self): anchor = self.getAnchor_copy() copied = anchor.copy() self.assertEqual(anchor.x, copied.x) def test_copy_same_y(self): anchor = self.getAnchor_copy() copied = anchor.copy() self.assertEqual(anchor.y, copied.y) # transform def test_transformBy_valid_no_origin(self): anchor = self.getAnchor_generic() anchor.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual(anchor.x, -1) self.assertEqual(anchor.y, 8) def test_transformBy_valid_origin(self): anchor = self.getAnchor_generic() anchor.transformBy((2, 0, 0, 2, 0, 0), origin=(1, 2)) self.assertEqual(anchor.x, 1) self.assertEqual(anchor.y, 2) def test_transformBy_invalid_one_string_value(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.transformBy((1, 0, 0, 1, 0, "0")) def test_transformBy_invalid_all_string_values(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.transformBy("1, 0, 0, 1, 0, 0") def test_transformBy_invalid_int_value(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.transformBy(123) # moveBy def test_moveBy_valid(self): anchor = self.getAnchor_generic() anchor.moveBy((-1, 2)) self.assertEqual(anchor.x, 0) self.assertEqual(anchor.y, 4) def test_moveBy_invalid_one_string_value(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.moveBy((-1, "2")) def test_moveBy_invalid_all_strings_value(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.moveBy("-1, 2") def test_moveBy_invalid_int_value(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.moveBy(1) # scaleBy def test_scaleBy_valid_one_value_no_origin(self): anchor = self.getAnchor_generic() anchor.scaleBy((-2)) self.assertEqual(anchor.x, -2) self.assertEqual(anchor.y, -4) def test_scaleBy_valid_two_values_no_origin(self): anchor = self.getAnchor_generic() anchor.scaleBy((-2, 3)) self.assertEqual(anchor.x, -2) self.assertEqual(anchor.y, 6) def test_scaleBy_valid_two_values_origin(self): anchor = self.getAnchor_generic() anchor.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual(anchor.x, 1) self.assertEqual(anchor.y, 2) def test_scaleBy_invalid_one_string_value(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.scaleBy((-1, "2")) def test_scaleBy_invalid_two_string_values(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.scaleBy("-1, 2") def test_scaleBy_invalid_tuple_too_many_values(self): anchor = self.getAnchor_generic() with self.assertRaises(ValueError): anchor.scaleBy((-1, 2, -3)) # rotateBy def test_rotateBy_valid_no_origin(self): anchor = self.getAnchor_generic() anchor.rotateBy(45) self.assertAlmostEqual(anchor.x, -0.707, places=3) self.assertAlmostEqual(anchor.y, 2.121, places=3) def test_rotateBy_valid_origin(self): anchor = self.getAnchor_generic() anchor.rotateBy(45, origin=(1, 2)) self.assertAlmostEqual(anchor.x, 1) self.assertAlmostEqual(anchor.y, 2) def test_rotateBy_invalid_string_value(self): anchor = self.getAnchor_generic() with self.assertRaises(TypeError): anchor.rotateBy("45") def test_rotateBy_invalid_too_large_value_positive(self): anchor = self.getAnchor_generic() with self.assertRaises(ValueError): anchor.rotateBy(361) def test_rotateBy_invalid_too_large_value_negative(self): anchor = self.getAnchor_generic() with self.assertRaises(ValueError): anchor.rotateBy(-361) # skewBy def test_skewBy_valid_no_origin_one_value(self): anchor = self.getAnchor_generic() anchor.skewBy(100) self.assertAlmostEqual(anchor.x, -10.343, places=3) self.assertEqual(anchor.y, 2.0) def test_skewBy_valid_no_origin_two_values(self): anchor = self.getAnchor_generic() anchor.skewBy((100, 200)) self.assertAlmostEqual(anchor.x, -10.343, places=3) self.assertAlmostEqual(anchor.y, 2.364, places=3) def test_skewBy_valid_origin_one_value(self): anchor = self.getAnchor_generic() anchor.skewBy(100, origin=(1, 2)) self.assertEqual(anchor.x, 1) self.assertEqual(anchor.y, 2) def test_skewBy_valid_origin_two_values(self): anchor = self.getAnchor_generic() anchor.skewBy((100, 200), origin=(1, 2)) self.assertEqual(anchor.x, 1) self.assertEqual(anchor.y, 2) # round def getAnchor_round(self): anchor = self.getAnchor_generic() anchor.x = 1.1 anchor.y = 2.5 return anchor def test_round_close_to(self): anchor = self.getAnchor_round() anchor.round() self.assertEqual(anchor.x, 1) def test_round_at_half(self): anchor = self.getAnchor_round() anchor.round() self.assertEqual(anchor.y, 3) # ---- # Hash # ---- def test_hash_object_self(self): anchor_one = self.getAnchor_generic() self.assertEqual( hash(anchor_one), hash(anchor_one) ) def test_hash_object_other(self): anchor_one = self.getAnchor_generic() anchor_two = self.getAnchor_generic() self.assertNotEqual( hash(anchor_one), hash(anchor_two) ) def test_hash_object_self_variable_assignment(self): anchor_one = self.getAnchor_generic() a = anchor_one self.assertEqual( hash(anchor_one), hash(a) ) def test_hash_object_other_variable_assignment(self): anchor_one = self.getAnchor_generic() anchor_two = self.getAnchor_generic() a = anchor_one self.assertNotEqual( hash(anchor_two), hash(a) ) def test_is_hashable(self): anchor_one = self.getAnchor_generic() self.assertTrue( isinstance(anchor_one, collections.abc.Hashable) ) # ------- # Parents # ------- def test_get_parent_font(self): font, _ = self.objectGenerator("font") layer = font.newLayer("L") glyph = layer.newGlyph("X") anchor = glyph.appendAnchor("anchor 0", (0, 0)) self.assertIsNotNone(anchor.font) self.assertEqual( anchor.font, font ) def test_get_parent_noFont(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") anchor = glyph.appendAnchor("anchor 0", (0, 0)) self.assertIsNone(anchor.font) def test_get_parent_layer(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") anchor = glyph.appendAnchor("anchor 0", (0, 0)) self.assertIsNotNone(anchor.layer) self.assertEqual( anchor.layer, layer ) def test_get_parent_noLayer(self): glyph, _ = self.objectGenerator("glyph") anchor = glyph.appendAnchor("anchor 0", (0, 0)) self.assertIsNone(anchor.font) self.assertIsNone(anchor.layer) def test_get_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") anchor = glyph.appendAnchor("anchor 0", (0, 0)) self.assertIsNotNone(anchor.glyph) self.assertEqual( anchor.glyph, glyph ) def test_get_parent_noGlyph(self): anchor, _ = self.objectGenerator("anchor") self.assertIsNone(anchor.font) self.assertIsNone(anchor.layer) self.assertIsNone(anchor.glyph) def test_set_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") anchor = self.getAnchor_generic() anchor.glyph = glyph self.assertIsNotNone(anchor.glyph) self.assertEqual( anchor.glyph, glyph ) def test_set_parent_glyph_none(self): anchor, _ = self.objectGenerator("anchor") anchor.glyph = None self.assertIsNone(anchor.glyph) def test_set_parent_glyph_exists(self): glyph, _ = self.objectGenerator("glyph") otherGlyph, _ = self.objectGenerator("glyph") anchor = glyph.appendAnchor("anchor 0", (0, 0)) with self.assertRaises(AssertionError): anchor.glyph = otherGlyph # -------- # Equality # -------- def test_object_equal_self(self): anchor_one = self.getAnchor_generic() self.assertEqual( anchor_one, anchor_one ) def test_object_not_equal_other(self): anchor_one = self.getAnchor_generic() anchor_two = self.getAnchor_generic() self.assertNotEqual( anchor_one, anchor_two ) def test_object_equal_variable_assignment_self(self): anchor_one = self.getAnchor_generic() a = anchor_one a.moveBy((-1, 2)) self.assertEqual( anchor_one, a ) def test_object_not_equal_variable_assignment_other(self): anchor_one = self.getAnchor_generic() anchor_two = self.getAnchor_generic() a = anchor_one self.assertNotEqual( anchor_two, a ) # --------- # Selection # --------- def test_selected_true(self): anchor = self.getAnchor_generic() try: anchor.selected = False except NotImplementedError: return anchor.selected = True self.assertEqual( anchor.selected, True ) def test_selected_false(self): anchor = self.getAnchor_generic() try: anchor.selected = False except NotImplementedError: return self.assertEqual( anchor.selected, False ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_bPoint.py000066400000000000000000001007101477533125200244560ustar00rootroot00000000000000import unittest import collections from fontParts.base import FontPartsError class TestBPoint(unittest.TestCase): def getBPoint_corner(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[1] return bPoint def getBPoint_corner_with_bcpOut(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((133, 212), "offcurve") contour.appendPoint((0, 0), "offcurve") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[1] return bPoint def getBPoint_corner_with_bcpIn(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((0, 0), "offcurve") contour.appendPoint((61, 190), "offcurve") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[1] return bPoint def getContour(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((19, 121), "offcurve") contour.appendPoint((61, 190), "offcurve") contour.appendPoint((101, 202), "curve", smooth=True) contour.appendPoint((133, 212), "offcurve") contour.appendPoint((155, 147), "offcurve") contour.appendPoint((255, 147), "curve") return contour def getBPoint_curve(self): contour = self.getContour() bPoint = contour.bPoints[1] return bPoint def getBPoint_curve_firstPointOpenContour(self): contour = self.getContour() bPoint = contour.bPoints[0] return bPoint def getBPoint_curve_lastPointOpenContour(self): contour = self.getContour() bPoint = contour.bPoints[-1] return bPoint def getBPoint_withName(self): bPoint = self.getBPoint_corner() bPoint.name = "BP" return bPoint # ---- # repr # ---- def test_reprContents(self): bPoint = self.getBPoint_corner() value = bPoint._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_noContour(self): point, _ = self.objectGenerator("point") value = point._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) # ------- # Parents # ------- def test_get_parent_font(self): font, _ = self.objectGenerator("font") layer = font.newLayer("L") glyph = layer.newGlyph("X") contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") glyph.appendContour(contour) contour = glyph.contours[0] bPoint = contour.bPoints[1] self.assertIsNotNone(bPoint.font) self.assertEqual( bPoint.font, font ) def test_get_parent_noFont(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") glyph.appendContour(contour) contour = glyph.contours[0] bPoint = contour.bPoints[1] self.assertIsNone(bPoint.font) def test_get_parent_layer(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") glyph.appendContour(contour) contour = glyph.contours[0] bPoint = contour.bPoints[1] self.assertIsNotNone(bPoint.layer) self.assertEqual( bPoint.layer, layer ) def test_get_parent_noLayer(self): glyph, _ = self.objectGenerator("glyph") contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") glyph.appendContour(contour) contour = glyph.contours[0] bPoint = contour.bPoints[1] self.assertIsNone(bPoint.font) self.assertIsNone(bPoint.layer) def test_get_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") glyph.appendContour(contour) contour = glyph.contours[0] bPoint = contour.bPoints[1] self.assertIsNotNone(bPoint.glyph) self.assertEqual( bPoint.glyph, glyph ) def test_get_parent_noGlyph(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[1] self.assertIsNone(bPoint.glyph) def test_get_parent_contour(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[1] self.assertIsNotNone(bPoint.contour) self.assertEqual( bPoint.contour, contour ) def test_get_parent_noContour(self): bPoint, _ = self.objectGenerator("bPoint") self.assertIsNone(bPoint.contour) def test_get_parent_segment(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[1] self.assertIsNotNone(bPoint._segment) # def test_get_parent_noSegment(self): # bPoint, _ = self.objectGenerator("bPoint") # self.assertIsNone(bPoint._segment) def test_get_parent_nextSegment(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[2] self.assertIsNotNone(bPoint._nextSegment) def test_get_parent_noNextSegment(self): bPoint, _ = self.objectGenerator("bPoint") self.assertIsNone(bPoint._nextSegment) # get segment/nosegment def test_set_parent_contour(self): contour, _ = self.objectGenerator("contour") bPoint, _ = self.objectGenerator("bPoint") bPoint.contour = contour self.assertIsNotNone(bPoint.contour) self.assertEqual( bPoint.contour, contour ) def test_set_already_set_parent_contour(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[1] contourOther, _ = self.objectGenerator("contour") with self.assertRaises(AssertionError): bPoint.contour = contourOther def test_set_parent_contour_none(self): bPoint, _ = self.objectGenerator("bPoint") bPoint.contour = None self.assertIsNone(bPoint.contour) def test_get_parent_glyph_noContour(self): bPoint, _ = self.objectGenerator("bPoint") self.assertIsNone(bPoint.glyph) def test_get_parent_layer_noContour(self): bPoint, _ = self.objectGenerator("bPoint") self.assertIsNone(bPoint.layer) def test_get_parent_font_noContour(self): bPoint, _ = self.objectGenerator("bPoint") self.assertIsNone(bPoint.font) # ---- # Attributes # ---- # type def test_get_type_corner(self): bPoint = self.getBPoint_corner() self.assertEqual( bPoint.type, "corner" ) def test_get_type_curve(self): bPoint = self.getBPoint_curve() self.assertEqual( bPoint.type, "curve" ) def test_set_type_corner(self): bPoint = self.getBPoint_curve() bPoint.type = "corner" self.assertEqual( bPoint.type, "corner" ) def test_set_type_curve(self): bPoint = self.getBPoint_corner() bPoint.type = "curve" self.assertEqual( bPoint.type, "curve" ) def test_type_not_equal(self): bPoint = self.getBPoint_corner() self.assertNotEqual( bPoint.type, "curve" ) def test_set_bcpOutIn_type_change(self): bPoint = self.getBPoint_curve() bPoint.bcpOut = (0, 0) bPoint.bcpIn = (0, 0) self.assertEqual( bPoint.type, "corner" ) def test_set_bcpInOut_type_change(self): bPoint = self.getBPoint_curve() bPoint.bcpIn = (0, 0) bPoint.bcpOut = (0, 0) self.assertEqual( bPoint.type, "corner" ) # https://github.com/robotools/fontParts/issues/435 def test_smooth_move_type_issue435(self): contour = self.getContour() contour.points[0].smooth = True bPoint = contour.bPoints[0] self.assertEqual( bPoint.type, "curve" ) # anchor def test_get_anchor(self): bPoint = self.getBPoint_corner() self.assertEqual( bPoint.anchor, (101, 202) ) def test_set_anchor_valid_tuple(self): bPoint = self.getBPoint_corner() bPoint.anchor = (51, 45) self.assertEqual( bPoint.anchor, (51, 45) ) def test_set_anchor_valid_list(self): bPoint = self.getBPoint_corner() bPoint.anchor = [51, 45] self.assertEqual( bPoint.anchor, (51, 45) ) def test_set_anchor_invalid_too_many_items(self): bPoint = self.getBPoint_corner() with self.assertRaises(ValueError): bPoint.anchor = (51, 45, 67) def test_set_anchor_invalid_single_item_list(self): bPoint = self.getBPoint_corner() with self.assertRaises(ValueError): bPoint.anchor = [51] def test_set_anchor_invalid_single_item_tuple(self): bPoint = self.getBPoint_corner() with self.assertRaises(ValueError): bPoint.anchor = (51,) def test_set_anchor_invalidType_int(self): bPoint = self.getBPoint_corner() with self.assertRaises(TypeError): bPoint.anchor = 51 def test_set_anchor_invalidType_None(self): bPoint = self.getBPoint_corner() with self.assertRaises(TypeError): bPoint.anchor = None # bcp in def test_get_bcpIn_corner(self): bPoint = self.getBPoint_corner() self.assertEqual( bPoint.bcpIn, (0, 0) ) def test_get_bcpIn_curve(self): bPoint = self.getBPoint_curve() self.assertEqual( bPoint.bcpIn, (-40, -12) ) def test_set_bcpIn_corner_valid_tuple(self): bPoint = self.getBPoint_corner() bPoint.bcpIn = (51, 45) self.assertEqual( bPoint.bcpIn, (51, 45) ) def test_set_bcpIn_corner_with_bcpOut(self): bPoint = self.getBPoint_corner_with_bcpOut() bPoint.bcpIn = (51, 45) self.assertEqual( bPoint.bcpIn, (51, 45) ) def test_set_bcpIn_curve_valid_tuple(self): bPoint = self.getBPoint_curve() bPoint.bcpIn = (51, 45) self.assertEqual( bPoint.bcpIn, (51, 45) ) def test_set_bcpIn_curve_firstPointOpenContour(self): bPoint = self.getBPoint_curve_firstPointOpenContour() with self.assertRaises(FontPartsError): bPoint.bcpIn = (10, 20) def test_set_bcpIn_valid_list(self): bPoint = self.getBPoint_corner() bPoint.bcpIn = [51, 45] self.assertEqual( bPoint.bcpIn, (51, 45) ) def test_set_bcpIn_invalid_too_many_items(self): bPoint = self.getBPoint_corner() with self.assertRaises(ValueError): bPoint.bcpIn = [51, 45, 67] def test_set_bcpIn_invalid_single_item_list(self): bPoint = self.getBPoint_corner() with self.assertRaises(ValueError): bPoint.bcpIn = [51] def test_set_bcpIn_invalid_single_item_tuple(self): bPoint = self.getBPoint_corner() with self.assertRaises(TypeError): bPoint.bcpIn = (51) def test_set_bcpIn_invalidType_int(self): bPoint = self.getBPoint_corner() with self.assertRaises(TypeError): bPoint.bcpIn = 51 def test_set_bcpIn_invalidType_None(self): bPoint = self.getBPoint_corner() with self.assertRaises(TypeError): bPoint.bcpIn = None # bcp out def test_get_bcpOut_corner(self): bPoint = self.getBPoint_corner() self.assertEqual( bPoint.bcpOut, (0, 0) ) def test_get_bcpOut_curve(self): bPoint = self.getBPoint_curve() self.assertEqual( bPoint.bcpOut, (32, 10) ) def test_set_bcpOut_corner_valid_tuple(self): bPoint = self.getBPoint_corner() bPoint.bcpOut = (51, 45) self.assertEqual( bPoint.bcpOut, (51, 45) ) def test_set_bcpOut_corner_with_bcpIn(self): bPoint = self.getBPoint_corner_with_bcpIn() bPoint.bcpOut = (51, 45) self.assertEqual( bPoint.bcpOut, (51, 45) ) def test_set_bcpOut_curve_valid_tuple(self): bPoint = self.getBPoint_curve() bPoint.bcpOut = (51, 45) self.assertEqual( bPoint.bcpOut, (51, 45) ) def test_set_bcpOut_valid_list(self): bPoint = self.getBPoint_curve() bPoint.bcpOut = [51, 45] self.assertEqual( bPoint.bcpOut, (51, 45) ) def test_set_bcpOut_curve_lastPointOpenContour(self): bPoint = self.getBPoint_curve_lastPointOpenContour() with self.assertRaises(FontPartsError): bPoint.bcpOut = (10, 20) def test_set_bcpOut_invalid_too_many_items(self): bPoint = self.getBPoint_corner() with self.assertRaises(ValueError): bPoint.bcpOut = [51, 45, 67] def test_set_bcpOut_invalid_single_item_list(self): bPoint = self.getBPoint_corner() with self.assertRaises(ValueError): bPoint.bcpOut = [51] def test_set_bcpOut_invalid_single_item_tuple(self): bPoint = self.getBPoint_corner() with self.assertRaises(TypeError): bPoint.bcpOut = (51) def test_set_bcpOut_invalidType_int(self): bPoint = self.getBPoint_corner() with self.assertRaises(TypeError): bPoint.bcpOut = 51 def test_set_bcpOut_invalidType_None(self): bPoint = self.getBPoint_corner() with self.assertRaises(TypeError): bPoint.bcpOut = None # -------------- # Identification # -------------- # index def getBPoint_noParentContour(self): bPoint, _ = self.objectGenerator("bPoint") bPoint.anchor = (101, 202) bPoint.bcpIn = (-40, 0) bPoint.bcpOut = (50, 0) bPoint.type = "curve" return bPoint def test_get_index(self): bPoint = self.getBPoint_corner() self.assertEqual( bPoint.index, 1 ) # def test_get_index_noParentContour(self): # bPoint = self.getBPoint_noParentContour() # self.assertEqual( # bPoint.index, # None # ) def test_set_index(self): point = self.getBPoint_corner() with self.assertRaises(FontPartsError): point.index = 0 # identifier def test_identifier_get_none(self): bPoint = self.getBPoint_corner() self.assertIsNone(bPoint.identifier) def test_identifier_generated_type(self): bPoint = self.getBPoint_corner() bPoint.getIdentifier() self.assertIsInstance(bPoint.identifier, str) def test_identifier_consistency(self): bPoint = self.getBPoint_corner() bPoint.getIdentifier() # get: twice to test consistency self.assertEqual(bPoint.identifier, bPoint.identifier) def test_identifier_cannot_set(self): # identifier is a read-only property bPoint = self.getBPoint_corner() with self.assertRaises(FontPartsError): bPoint.identifier = "ABC" # def test_getIdentifer_no_contour(self): # bPoint, _ = self.objectGenerator("bPoint") # with self.assertRaises(FontPartsError): # bPoint.getIdentifier() def test_getIdentifer_consistency(self): bPoint = self.getBPoint_corner() bPoint.getIdentifier() self.assertEqual(bPoint.identifier, bPoint.getIdentifier()) # ---- # Hash # ---- def test_hash(self): bPoint = self.getBPoint_corner() self.assertEqual( isinstance(bPoint, collections.abc.Hashable), False ) # -------- # Equality # -------- def test_object_equal_self(self): bPoint_one = self.getBPoint_corner() self.assertEqual( bPoint_one, bPoint_one ) def test_object_not_equal_other(self): bPoint_one = self.getBPoint_corner() bPoint_two = self.getBPoint_corner() self.assertNotEqual( bPoint_one, bPoint_two ) def test_object_equal_self_variable_assignment(self): bPoint_one = self.getBPoint_corner() a = bPoint_one a.anchor = (51, 45) self.assertEqual( bPoint_one, a ) def test_object_not_equal_other_variable_assignment(self): bPoint_one = self.getBPoint_corner() bPoint_two = self.getBPoint_corner() a = bPoint_one self.assertNotEqual( bPoint_two, a ) # --------- # Selection # --------- def test_selected_true(self): bPoint = self.getBPoint_corner() try: bPoint.selected = False except NotImplementedError: return bPoint.selected = True self.assertEqual( bPoint.selected, True ) def test_selected_false(self): bPoint = self.getBPoint_corner() try: bPoint.selected = False except NotImplementedError: return bPoint.selected = False self.assertEqual( bPoint.selected, False ) # ---- # Copy # ---- def test_copy_seperate_objects(self): bPoint = self.getBPoint_corner() copied = bPoint.copy() self.assertIsNot( bPoint, copied ) def test_copy_different_contour(self): bPoint = self.getBPoint_corner() copied = bPoint.copy() self.assertIsNot( bPoint.contour, copied.contour ) def test_copy_none_contour(self): bPoint = self.getBPoint_corner() copied = bPoint.copy() self.assertEqual( copied.contour, None ) # def test_copy_same_type(self): # bPoint = self.getBPoint_corner() # copied = bPoint.copy() # self.assertEqual( # bPoint.type, # copied.type # ) # def test_copy_same_anchor(self): # bPoint = self.getBPoint_corner() # copied = bPoint.copy() # self.assertEqual( # bPoint.anchor, # copied.anchor # ) # def test_copy_same_bcpIn(self): # bPoint = self.getBPoint_corner() # copied = bPoint.copy() # self.assertEqual( # bPoint.bcpIn, # copied.bcpIn # ) # def test_copy_same_bcpOut(self): # bPoint = self.getBPoint_corner() # copied = bPoint.copy() # self.assertEqual( # bPoint.bcpOut, # copied.bcpOut # ) # def test_copy_same_identifier_None(self): # bPoint = self.getBPoint_corner() # bPoint.identifer = None # copied = bPoint.copy() # self.assertEqual( # bPoint.identifier, # copied.identifier, # ) # def test_copy_different_identifier(self): # bPoint = self.getBPoint_corner() # bPoint.getIdentifier() # copied = bPoint.copy() # self.assertNotEqual( # bPoint.identifier, # copied.identifier, # ) # def test_copy_generated_identifier_different(self): # otherContour, _ = self.objectGenerator("contour") # bPoint = self.getBPoint_corner() # copied = bPoint.copy() # copied.contour = otherContour # bPoint.getIdentifier() # copied.getIdentifier() # self.assertNotEqual( # bPoint.identifier, # copied.identifier # ) # def test_copyData_type(self): # bPoint = self.getBPoint_corner() # bPointOther, _ = self.objectGenerator("bPoint") # bPointOther.copyData(bPoint) # self.assertEqual( # bPoint.type, # bPointOther.type, # ) # def test_copyData_anchor(self): # bPoint = self.getBPoint_corner() # bPointOther, _ = self.objectGenerator("bPoint") # bPointOther.copyData(bPoint) # self.assertEqual( # bPoint.anchor, # bPointOther.anchor, # ) # def test_copyData_bcpIn(self): # bPoint = self.getBPoint_corner() # bPointOther, _ = self.objectGenerator("bPoint") # bPointOther.copyData(bPoint) # self.assertEqual( # bPoint.bcpIn, # bPointOther.bcpIn, # ) # def test_copyData_bcpOut(self): # bPoint = self.getBPoint_corner() # bPointOther, _ = self.objectGenerator("bPoint") # bPointOther.copyData(bPoint) # self.assertEqual( # bPoint.bcpOut, # bPointOther.bcpOut, # ) # -------------- # Transformation # -------------- # transformBy def test_transformBy_valid_no_origin_anchor(self): bPoint = self.getBPoint_curve() bPoint.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual( bPoint.anchor, (199.0, 608.0) ) def test_transformBy_valid_no_origin_bcpIn(self): bPoint = self.getBPoint_curve() bPoint.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual( bPoint.bcpIn, (-80.0, -36.0) ) def test_transformBy_valid_no_origin_bcpOut(self): bPoint = self.getBPoint_curve() bPoint.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual( bPoint.bcpOut, (64.0, 30.0) ) def test_transformBy_valid_origin_anchor(self): bPoint = self.getBPoint_curve() bPoint.transformBy((2, 0, 0, 2, 0, 0), origin=(1, 2)) self.assertEqual( bPoint.anchor, (201.0, 402.0) ) def test_transformBy_valid_origin_bcpIn(self): bPoint = self.getBPoint_curve() bPoint.transformBy((2, 0, 0, 2, 0, 0), origin=(1, 2)) self.assertEqual( bPoint.bcpIn, (-80.0, -24.0) ) def test_transformBy_valid_origin_bcpOut(self): bPoint = self.getBPoint_curve() bPoint.transformBy((2, 0, 0, 2, 0, 0), origin=(1, 2)) self.assertEqual( bPoint.bcpOut, (64.0, 20.0) ) def test_transformBy_invalid_one_string_value(self): point = self.getBPoint_curve() with self.assertRaises(TypeError): point.transformBy((1, 0, 0, 1, 0, "0")) def test_transformBy_invalid_all_string_values(self): point = self.getBPoint_curve() with self.assertRaises(TypeError): point.transformBy("1, 0, 0, 1, 0, 0") def test_transformBy_invalid_int_value(self): point = self.getBPoint_curve() with self.assertRaises(TypeError): point.transformBy(123) # moveBy def test_moveBy_valid_anchor(self): bPoint = self.getBPoint_curve() bPoint.moveBy((-1, 2)) self.assertEqual( bPoint.anchor, (100.0, 204.0) ) def test_moveBy_noChange_bcpIn(self): bPoint = self.getBPoint_curve() bPoint.moveBy((-1, 2)) otherBPoint = self.getBPoint_curve() self.assertEqual( bPoint.bcpIn, otherBPoint.bcpIn ) def test_moveBy_noChange_bcpOut(self): bPoint = self.getBPoint_curve() bPoint.moveBy((-1, 2)) otherBPoint = self.getBPoint_curve() self.assertEqual( bPoint.bcpOut, otherBPoint.bcpOut ) def test_moveBy_invalid_one_string_value(self): bPoint = self.getBPoint_curve() with self.assertRaises(TypeError): bPoint.moveBy((-1, "2")) def test_moveBy_invalid_all_strings_value(self): bPoint = self.getBPoint_curve() with self.assertRaises(TypeError): bPoint.moveBy("-1, 2") def test_moveBy_invalid_int_value(self): bPoint = self.getBPoint_curve() with self.assertRaises(TypeError): bPoint.moveBy(1) # scaleBy def test_scaleBy_valid_one_value_no_origin_anchor(self): bPoint = self.getBPoint_curve() bPoint.scaleBy((-2)) self.assertEqual( bPoint.anchor, (-202.0, -404.0) ) def test_scaleBy_valid_two_values_no_origin_anchor(self): bPoint = self.getBPoint_curve() bPoint.scaleBy((-2, 3)) self.assertEqual( bPoint.anchor, (-202.0, 606.0) ) def test_scaleBy_valid_two_values_origin_anchor(self): bPoint = self.getBPoint_curve() bPoint.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual( bPoint.anchor, (-199.0, 602.0) ) def test_scaleBy_valid_two_values_origin_bcpIn(self): bPoint = self.getBPoint_curve() bPoint.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual( bPoint.bcpIn, (80.0, -36.0) ) def test_scaleBy_valid_two_values_origin_bcpOut(self): bPoint = self.getBPoint_curve() bPoint.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual( bPoint.bcpOut, (-64.0, 30.0) ) def test_invalid_one_string_value_scaleBy(self): bPoint = self.getBPoint_curve() with self.assertRaises(TypeError): bPoint.scaleBy((-1, "2")) def test_invalid_two_string_values_scaleBy(self): bPoint = self.getBPoint_curve() with self.assertRaises(TypeError): bPoint.scaleBy("-1, 2") def test_invalid_tuple_too_many_values_scaleBy(self): bPoint = self.getBPoint_curve() with self.assertRaises(ValueError): bPoint.scaleBy((-1, 2, -3)) # rotateBy def test_rotateBy_valid_no_origin_anchor(self): bPoint = self.getBPoint_curve() bPoint.rotateBy(45) self.assertEqual( [(round(bPoint.anchor[0], 3)), (round(bPoint.anchor[1], 3))], [-71.418, 214.253] ) def test_rotateBy_valid_origin_anchor(self): bPoint = self.getBPoint_curve() bPoint.rotateBy(45, origin=(1, 2)) self.assertEqual( [(round(bPoint.anchor[0], 3)), (round(bPoint.anchor[1], 3))], [-69.711, 214.132] ) def test_rotateBy_valid_origin_bcpIn(self): bPoint = self.getBPoint_curve() bPoint.rotateBy(45, origin=(1, 2)) self.assertEqual( [(round(bPoint.bcpIn[0], 3)), (round(bPoint.bcpIn[1], 3))], [-19.799, -36.77] ) def test_rotateBy_valid_origin_bcpOut(self): bPoint = self.getBPoint_curve() bPoint.rotateBy(45, origin=(1, 2)) self.assertEqual( [(round(bPoint.bcpOut[0], 3)), (round(bPoint.bcpOut[1], 3))], [15.556, 29.698] ) def test_rotateBy_invalid_string_value(self): bPoint = self.getBPoint_curve() with self.assertRaises(TypeError): bPoint.rotateBy("45") def test_rotateBy_invalid_too_large_value_positive(self): bPoint = self.getBPoint_curve() with self.assertRaises(ValueError): bPoint.rotateBy(361) def test_rotateBy_invalid_too_large_value_negative(self): bPoint = self.getBPoint_curve() with self.assertRaises(ValueError): bPoint.rotateBy(-361) # skewBy def test_skewBy_valid_no_origin_one_value_anchor(self): bPoint = self.getBPoint_curve() bPoint.skewBy(100) self.assertEqual( [(round(bPoint.anchor[0], 3)), (round(bPoint.anchor[1], 3))], [-1044.599, 202.0] ) def test_skewBy_valid_no_origin_two_values_anchor(self): bPoint = self.getBPoint_curve() bPoint.skewBy((100, 200)) self.assertEqual( [(round(bPoint.anchor[0], 3)), (round(bPoint.anchor[1], 3))], [-1044.599, 238.761] ) def test_skewBy_valid_origin_one_value_anchor(self): bPoint = self.getBPoint_curve() bPoint.skewBy(100, origin=(1, 2)) self.assertEqual( [(round(bPoint.anchor[0], 3)), (round(bPoint.anchor[1], 3))], [-1033.256, 202.0] ) def test_skewBy_valid_origin_two_values_anchor(self): bPoint = self.getBPoint_curve() bPoint.skewBy((100, 200), origin=(1, 2)) self.assertEqual( [(round(bPoint.anchor[0], 3)), (round(bPoint.anchor[1], 3))], [-1033.256, 238.397] ) def test_skewBy_valid_origin_two_values_bcpIn(self): bPoint = self.getBPoint_curve() bPoint.skewBy((100, 200), origin=(1, 2)) self.assertEqual( [(round(bPoint.bcpIn[0], 3)), (round(bPoint.bcpIn[1], 3))], [28.055, -26.559] ) def test_skewBy_valid_origin_two_values_bcpOut(self): bPoint = self.getBPoint_curve() bPoint.skewBy((100, 200), origin=(1, 2)) self.assertEqual( [(round(bPoint.bcpOut[0], 3)), (round(bPoint.bcpOut[1], 3))], [-24.713, 21.647] ) def test_skewBy_invalid_string_value(self): bPoint = self.getBPoint_curve() with self.assertRaises(TypeError): bPoint.skewBy("45") def test_skewBy_invalid_too_large_value_positive(self): bPoint = self.getBPoint_curve() with self.assertRaises(ValueError): bPoint.skewBy(361) def test_skewBy_invalid_too_large_value_negative(self): bPoint = self.getBPoint_curve() with self.assertRaises(ValueError): bPoint.skewBy(-361) # ------------- # Normalization # ------------- # round def getBPoint_curve_float(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((19.231, 121.291), "offcurve") contour.appendPoint((61.193, 190.942), "offcurve") contour.appendPoint((101.529, 202.249), "curve", smooth=True) contour.appendPoint((133.948, 212.193), "offcurve") contour.appendPoint((155.491, 147.314), "offcurve") contour.appendPoint((255.295, 147.314), "curve") bPoint = contour.bPoints[1] return bPoint def test_round_anchor(self): bPoint = self.getBPoint_curve_float() bPoint.round() self.assertEqual( bPoint.anchor, (102.0, 202.0) ) def test_round_bcpIn(self): bPoint = self.getBPoint_curve_float() bPoint.round() self.assertEqual( bPoint.bcpIn, (-40.0, -11.0) ) def test_round_bcpOut(self): bPoint = self.getBPoint_curve_float() bPoint.round() self.assertEqual( bPoint.bcpOut, (32.0, 10.0) ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_color.py000066400000000000000000000010121477533125200243340ustar00rootroot00000000000000import unittest from fontParts.base.color import Color class TestComponent(unittest.TestCase): def test_color_r(self): color = Color((1.0, 0, 0, 0)) self.assertEqual(color.r, 1.0) def test_color_g(self): color = Color((0, 1.0, 0, 0)) self.assertEqual(color.g, 1.0) def test_color_b(self): color = Color((0, 0, 1.0, 0)) self.assertEqual(color.b, 1.0) def test_color_a(self): color = Color((0, 0, 0, 1.00)) self.assertEqual(color.a, 1.0) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_component.py000066400000000000000000000663421477533125200252410ustar00rootroot00000000000000import unittest import collections from fontParts.base import FontPartsError class TestComponent(unittest.TestCase): def getComponent_generic(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("A") pen = glyph.getPen() pen.moveTo((0, 0)) pen.lineTo((0, 100)) pen.lineTo((100, 100)) pen.lineTo((100, 0)) pen.closePath() for i, point in enumerate(glyph[0].points): point.name = "point %d" % i glyph = layer.newGlyph("B") component = glyph.appendComponent("A") component.transformation = (1, 0, 0, 1, 0, 0) return component # ---- # repr # ---- def test_reprContents(self): component = self.getComponent_generic() value = component._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_noGlyph(self): component, _ = self.objectGenerator("component") value = component._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) # ------- # Parents # ------- def test_get_parent_font(self): font, _ = self.objectGenerator("font") layer = font.newLayer("L") glyph = layer.newGlyph("X") component = glyph.appendComponent("A") self.assertIsNotNone(component.font) self.assertEqual( component.font, font ) def test_get_parent_noFont(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") component = glyph.appendComponent("A") self.assertIsNone(component.font) def test_get_parent_layer(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") component = glyph.appendComponent("A") self.assertIsNotNone(component.layer) self.assertEqual( component.layer, layer ) def test_get_parent_noLayer(self): glyph, _ = self.objectGenerator("glyph") component = glyph.appendComponent("A") self.assertIsNone(component.font) self.assertIsNone(component.layer) def test_get_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") component = glyph.appendComponent("A") self.assertIsNotNone(component.glyph) self.assertEqual( component.glyph, glyph ) def test_get_parent_noGlyph(self): component, _ = self.objectGenerator("component") self.assertIsNone(component.font) self.assertIsNone(component.layer) self.assertIsNone(component.glyph) def test_set_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") component, _ = self.objectGenerator("component") component.glyph = glyph self.assertIsNotNone(component.glyph) self.assertEqual( component.glyph, glyph ) def test_set_parent_glyph_none(self): component, _ = self.objectGenerator("component") component.glyph = None self.assertIsNone(component.glyph) def test_set_parent_glyph_exists(self): glyph, _ = self.objectGenerator("glyph") otherGlyph, _ = self.objectGenerator("glyph") component = glyph.appendComponent("A") with self.assertRaises(AssertionError): component.glyph = otherGlyph # ---------- # Attributes # ---------- # baseGlyph def test_baseGlyph_generic(self): component = self.getComponent_generic() self.assertEqual( component.baseGlyph, "A" ) def test_baseGlyph_valid_set(self): component = self.getComponent_generic() component.baseGlyph = "B" self.assertEqual( component.baseGlyph, "B" ) def test_baseGlyph_invalid_set_none(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.baseGlyph = None def test_baseGlyph_invalid_set_empty_string(self): component = self.getComponent_generic() with self.assertRaises(ValueError): component.baseGlyph = "" def test_baseGlyph_invalid_set_int(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.baseGlyph = 123 # transformation def test_transformation_generic(self): component = self.getComponent_generic() self.assertEqual( component.transformation, (1, 0, 0, 1, 0, 0) ) def test_transformation_valid_set_positive(self): component = self.getComponent_generic() component.transformation = (1, 2, 3, 4, 5, 6) self.assertEqual( component.transformation, (1, 2, 3, 4, 5, 6) ) def test_transformation_valid_set_negative(self): component = self.getComponent_generic() component.transformation = (-1, -2, -3, -4, -5, -6) self.assertEqual( component.transformation, (-1, -2, -3, -4, -5, -6) ) def test_transformation_invalid_set_member(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.transformation = (1, 0, 0, 1, 0, "0") def test_transformation_invalid_set_none(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.transformation = None # offset def test_offset_generic(self): component = self.getComponent_generic() self.assertEqual( component.offset, (0, 0) ) def test_offset_valid_set_zero(self): component = self.getComponent_generic() component.offset = (0, 0) self.assertEqual( component.offset, (0, 0) ) def test_offset_valid_set_positive_positive(self): component = self.getComponent_generic() component.offset = (1, 2) self.assertEqual( component.offset, (1, 2) ) def test_offset_valid_set_negative_positive(self): component = self.getComponent_generic() component.offset = (-1, 2) self.assertEqual( component.offset, (-1, 2) ) def test_offset_valid_set_positive_negative(self): component = self.getComponent_generic() component.offset = (1, -2) self.assertEqual( component.offset, (1, -2) ) def test_offset_valid_set_negative_negative(self): component = self.getComponent_generic() component.offset = (-1, -2) self.assertEqual( component.offset, (-1, -2) ) def test_offset_invalid_set_real_bogus(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.offset = (1, "2") def test_offset_invalid_set_bogus_real(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.offset = ("1", 2) def test_offset_invalid_set_int(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.offset = 1 def test_offset_invalid_set_none(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.offset = None # scale def test_scale_generic(self): component = self.getComponent_generic() self.assertEqual( component.scale, (1, 1) ) def test_scale_valid_set_zero(self): component = self.getComponent_generic() component.scale = (0, 0) self.assertEqual( component.scale, (0, 0) ) def test_scale_valid_set_positive_positive(self): component = self.getComponent_generic() component.scale = (1, 2) self.assertEqual( component.scale, (1, 2) ) def test_scale_valid_set_negative_positive(self): component = self.getComponent_generic() component.scale = (-1, 2) self.assertEqual( component.scale, (-1, 2) ) def test_scale_valid_set_positive_negative(self): component = self.getComponent_generic() component.scale = (1, -2) self.assertEqual( component.scale, (1, -2) ) def test_scale_valid_set_negative_negative(self): component = self.getComponent_generic() component.scale = (-1, -2) self.assertEqual( component.scale, (-1, -2) ) def test_scale_invalid_set_real_bogus(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.scale = (1, "2") def test_scale_invalid_set_bogus_real(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.scale = ("1", 2) def test_scale_invalid_set_int(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.scale = 1 def test_scale_invalid_set_none(self): component = self.getComponent_generic() with self.assertRaises(TypeError): component.scale = None # -------------- # Identification # -------------- # index def getComponent_index(self): glyph, _ = self.objectGenerator("glyph") glyph.appendComponent("A") glyph.appendComponent("B") glyph.appendComponent("C") return glyph def test_get_index_noParent(self): component, _ = self.objectGenerator("component") self.assertIsNone(component.index) def test_get_index(self): glyph = self.getComponent_index() for i, component in enumerate(glyph.components): self.assertEqual(component.index, i) def test_set_index_noParent(self): component, _ = self.objectGenerator("component") with self.assertRaises(FontPartsError): component.index = 1 def test_set_index_positive(self): glyph = self.getComponent_index() component = glyph.components[0] component.index = 2 self.assertEqual( [c.baseGlyph for c in glyph.components], ["B", "A", "C"] ) def test_set_index_pastLength(self): glyph = self.getComponent_index() component = glyph.components[0] component.index = 20 self.assertEqual( [c.baseGlyph for c in glyph.components], ["B", "C", "A"] ) def test_set_index_negative(self): glyph = self.getComponent_index() component = glyph.components[1] component.index = -1 self.assertEqual( [c.baseGlyph for c in glyph.components], ["B", "A", "C"] ) # identifier def test_identifier_get_none(self): component = self.getComponent_generic() self.assertIsNone(component.identifier) def test_identifier_generated_type(self): component = self.getComponent_generic() component.getIdentifier() self.assertIsInstance(component.identifier, str) def test_identifier_consistency(self): component = self.getComponent_generic() component.getIdentifier() # get: twice to test consistency self.assertEqual(component.identifier, component.identifier) def test_identifier_cannot_set(self): # identifier is a read-only property component = self.getComponent_generic() with self.assertRaises(FontPartsError): component.identifier = "ABC" # ---- # Copy # ---- def getComponent_copy(self): component = self.getComponent_generic() component.transformation = (1, 2, 3, 4, 5, 6) return component def test_copy_seperate_objects(self): component = self.getComponent_copy() copied = component.copy() self.assertIsNot(component, copied) def test_copy_same_baseGlyph(self): component = self.getComponent_copy() copied = component.copy() self.assertEqual(component.baseGlyph, copied.baseGlyph) def test_copy_same_transformation(self): component = self.getComponent_copy() copied = component.copy() self.assertEqual(component.transformation, copied.transformation) def test_copy_same_offset(self): component = self.getComponent_copy() copied = component.copy() self.assertEqual(component.offset, copied.offset) def test_copy_same_scale(self): component = self.getComponent_copy() copied = component.copy() self.assertEqual(component.scale, copied.scale) def test_copy_not_identifier(self): component = self.getComponent_copy() component.getIdentifier() copied = component.copy() self.assertNotEqual(component.identifier, copied.identifier) def test_copy_generated_identifier_different(self): component = self.getComponent_copy() copied = component.copy() component.getIdentifier() copied.getIdentifier() self.assertNotEqual(component.identifier, copied.identifier) # ---- # Pens # ---- # draw def test_draw(self): from fontTools.pens.recordingPen import RecordingPen component = self.getComponent_generic() component.transformation = (1, 2, 3, 4, 5, 6) pen = RecordingPen() component.draw(pen) expected = [('addComponent', ('A', (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)))] self.assertEqual( pen.value, expected ) # drawPoints def test_drawPoints(self): from fontPens.recordingPointPen import RecordingPointPen component = self.getComponent_generic() component.transformation = (1, 2, 3, 4, 5, 6) identifier = component.getIdentifier() pointPen = RecordingPointPen() component.drawPoints(pointPen) expected = [('addComponent', (u'A', (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)), {'identifier': identifier})] self.assertEqual( pointPen.value, expected ) def test_drawPoints_legacy(self): from .legacyPointPen import LegacyPointPen component = self.getComponent_generic() component.transformation = (1, 2, 3, 4, 5, 6) component.getIdentifier() pointPen = LegacyPointPen() component.drawPoints(pointPen) expected = [('addComponent', (u'A', (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)), {})] self.assertEqual( pointPen.value, expected ) # -------------- # Transformation # -------------- def getComponent_transform(self): component = self.getComponent_generic() component.transformation = (1, 2, 3, 4, 5, 6) return component # transformBy def test_transformBy_valid_no_origin(self): component = self.getComponent_transform() component.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual( component.transformation, (2.0, 6.0, 6.0, 12.0, 7.0, 20.0) ) def test_transformBy_valid_origin(self): component = self.getComponent_transform() component.transformBy((2, 0, 0, 2, 0, 0), origin=(1, 2)) self.assertEqual( component.transformation, (2.0, 4.0, 6.0, 8.0, 9.0, 10.0) ) def test_transformBy_invalid_one_string_value(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.transformBy((1, 0, 0, 1, 0, "0")) def test_transformBy_invalid_all_string_values(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.transformBy("1, 0, 0, 1, 0, 0") def test_transformBy_invalid_int_value(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.transformBy(123) # moveBy def test_moveBy_valid(self): component = self.getComponent_transform() component.moveBy((-1, 2)) self.assertEqual( component.transformation, (1.0, 2.0, 3.0, 4.0, 4.0, 8.0) ) def test_moveBy_invalid_one_string_value(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.moveBy((-1, "2")) def test_moveBy_invalid_all_strings_value(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.moveBy("-1, 2") def test_moveBy_invalid_int_value(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.moveBy(1) # scaleBy def test_scaleBy_valid_one_value_no_origin(self): component = self.getComponent_transform() component.scaleBy((-2)) self.assertEqual( component.transformation, (-2.0, -4.0, -6.0, -8.0, -10.0, -12.0) ) def test_scaleBy_valid_two_values_no_origin(self): component = self.getComponent_transform() component.scaleBy((-2, 3)) self.assertEqual( component.transformation, (-2.0, 6.0, -6.0, 12.0, -10.0, 18.0) ) def test_scaleBy_valid_two_values_origin(self): component = self.getComponent_transform() component.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual( component.transformation, (-2.0, 6.0, -6.0, 12.0, -7.0, 14.0) ) def test_scaleBy_invalid_one_string_value(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.scaleBy((-1, "2")) def test_scaleBy_invalid_two_string_values(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.scaleBy("-1, 2") def test_scaleBy_invalid_tuple_too_many_values(self): component = self.getComponent_transform() with self.assertRaises(ValueError): component.scaleBy((-1, 2, -3)) # # rotateBy def test_rotateBy_valid_no_origin(self): component = self.getComponent_transform() component.rotateBy(45) self.assertEqual( [round(i, 3) for i in component.transformation], [-0.707, 2.121, -0.707, 4.95, -0.707, 7.778] ) def test_rotateBy_valid_origin(self): component = self.getComponent_transform() component.rotateBy(45, origin=(1, 2)) self.assertEqual( [round(i, 3) for i in component.transformation], [-0.707, 2.121, -0.707, 4.95, 1.0, 7.657] ) def test_rotateBy_invalid_string_value(self): component = self.getComponent_transform() with self.assertRaises(TypeError): component.rotateBy("45") def test_rotateBy_invalid_too_large_value_positive(self): component = self.getComponent_transform() with self.assertRaises(ValueError): component.rotateBy(361) def test_rotateBy_invalid_too_large_value_negative(self): component = self.getComponent_transform() with self.assertRaises(ValueError): component.rotateBy(-361) # skewBy def test_skewBy_valid_no_origin_one_value(self): component = self.getComponent_transform() component.skewBy(100) self.assertEqual( [round(i, 3) for i in component.transformation], [-10.343, 2.0, -19.685, 4.0, -29.028, 6.0] ) def test_skewBy_valid_no_origin_two_values(self): component = self.getComponent_transform() component.skewBy((100, 200)) self.assertEqual( [round(i, 3) for i in component.transformation], [-10.343, 2.364, -19.685, 5.092, -29.028, 7.82] ) def test_skewBy_valid_origin_one_value(self): component = self.getComponent_transform() component.skewBy(100, origin=(1, 2)) self.assertEqual( [round(i, 3) for i in component.transformation], [-10.343, 2.0, -19.685, 4.0, -17.685, 6.0] ) def test_skewBy_valid_origin_two_values(self): component = self.getComponent_transform() component.skewBy((100, 200), origin=(1, 2)) self.assertEqual( [round(i, 3) for i in component.transformation], [-10.343, 2.364, -19.685, 5.092, -17.685, 7.456] ) # ------------- # Normalization # ------------- # round def test_round(self): component = self.getComponent_generic() component.transformation = (1.2, 2.2, 3.3, 4.4, 5.1, 6.6) component.round() self.assertEqual( component.transformation, (1.2, 2.2, 3.3, 4.4, 5, 7) ) # decompose def test_decompose_noParent(self): component, _ = self.objectGenerator("component") with self.assertRaises(FontPartsError): component.decompose() def test_decompose_digest(self): from fontPens.digestPointPen import DigestPointPen component = self.getComponent_generic() glyph = component.glyph glyph.layer[component.baseGlyph] component.decompose() pointPen = DigestPointPen() glyph.drawPoints(pointPen) expected = ( ('beginPath', None), ((0, 0), u'line', False, 'point 0'), ((0, 100), u'line', False, 'point 1'), ((100, 100), u'line', False, 'point 2'), ((100, 0), u'line', False, 'point 3'), 'endPath' ) self.assertEqual( pointPen.getDigest(), expected ) def test_decompose_identifiers(self): component = self.getComponent_generic() glyph = component.glyph baseGlyph = glyph.layer[component.baseGlyph] baseGlyph[0].getIdentifier() for point in baseGlyph[0].points: point.getIdentifier() component.decompose() self.assertEqual( [point.identifier for point in glyph[0].points], [point.identifier for point in baseGlyph[0].points] ) self.assertEqual( glyph[0].identifier, baseGlyph[0].identifier ) def test_decompose_transformation(self): from fontPens.digestPointPen import DigestPointPen component = self.getComponent_generic() component.scale = (2, 2) glyph = component.glyph glyph.layer[component.baseGlyph] component.decompose() pointPen = DigestPointPen() glyph.drawPoints(pointPen) expected = ( ('beginPath', None), ((0, 0), u'line', False, 'point 0'), ((0, 200), u'line', False, 'point 1'), ((200, 200), u'line', False, 'point 2'), ((200, 0), u'line', False, 'point 3'), 'endPath' ) self.assertEqual( pointPen.getDigest(), expected ) # ------------ # Data Queries # ------------ # bounds def test_bounds_get(self): component = self.getComponent_generic() self.assertEqual( component.bounds, (0, 0, 100, 100) ) def test_bounds_none(self): component = self.getComponent_generic() layer = component.layer baseGlyph = layer[component.baseGlyph] baseGlyph.clear() self.assertIsNone(component.bounds) def test_bounds_on_move(self): component = self.getComponent_generic() component.moveBy((0.1, -0.1)) self.assertEqual( component.bounds, (0.1, -0.1, 100.1, 99.9) ) def test_bounds_on_scale(self): component = self.getComponent_generic() component.scaleBy((2, 0.5)) self.assertEqual( component.bounds, (0, 0, 200, 50) ) def test_bounds_invalid_set(self): component = self.getComponent_generic() with self.assertRaises(FontPartsError): component.bounds = (0, 0, 100, 100) # pointInside def test_pointInside_true(self): component = self.getComponent_generic() self.assertEqual( component.pointInside((1, 1)), True ) def test_pointInside_false(self): component = self.getComponent_generic() self.assertEqual( component.pointInside((-1, -1)), False ) # ---- # Hash # ---- def test_hash_object_self(self): component_one = self.getComponent_generic() self.assertEqual( hash(component_one), hash(component_one) ) def test_hash_object_other(self): component_one = self.getComponent_generic() component_two = self.getComponent_generic() self.assertNotEqual( hash(component_one), hash(component_two) ) def test_hash_object_self_variable_assignment(self): component_one = self.getComponent_generic() a = component_one self.assertEqual( hash(component_one), hash(a) ) def test_hash_object_other_variable_assignment(self): component_one = self.getComponent_generic() component_two = self.getComponent_generic() a = component_one self.assertNotEqual( hash(component_two), hash(a) ) def test_is_hashable(self): component_one = self.getComponent_generic() self.assertTrue( isinstance(component_one, collections.abc.Hashable) ) # -------- # Equality # -------- def test_object_equal_self(self): component_one = self.getComponent_generic() self.assertEqual( component_one, component_one ) def test_object_not_equal_other(self): component_one = self.getComponent_generic() component_two = self.getComponent_generic() self.assertNotEqual( component_one, component_two ) def test_object_equal_assigned_variable(self): component_one = self.getComponent_generic() a = component_one a.baseGlyph = "C" self.assertEqual( component_one, a ) def test_object_not_equal_assigned_variable_other(self): component_one = self.getComponent_generic() component_two = self.getComponent_generic() a = component_one self.assertNotEqual( component_two, a ) # --------- # Selection # --------- def test_selected_true(self): component = self.getComponent_generic() try: component.selected = False except NotImplementedError: return component.selected = True self.assertEqual( component.selected, True ) def test_selected_false(self): component = self.getComponent_generic() try: component.selected = False except NotImplementedError: return self.assertEqual( component.selected, False ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_contour.py000066400000000000000000000542221477533125200247220ustar00rootroot00000000000000import unittest import collections from fontParts.base import FontPartsError class TestContour(unittest.TestCase): def getContour_bounds(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "line") contour.appendPoint((0, 100), "line") contour.appendPoint((100, 100), "line") contour.appendPoint((100, 0), "line") return contour # ---- # Repr # ---- def test_reprContents_noGlyph_noID(self): contour = self.getContour_bounds() value = contour._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_noGlyph_ID(self): contour = self.getContour_bounds() contour.getIdentifier() value = contour._reprContents() self.assertIsInstance(value, list) idFound = False for i in value: self.assertIsInstance(i, str) if i == "identifier='%r'" % contour.identifier: idFound = True self.assertTrue(idFound) def test_reprContents_Glyph_ID(self): glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() contour.glyph = glyph contour.getIdentifier() value = contour._reprContents() self.assertIsInstance(value, list) idFound = False glyphFound = False for i in value: self.assertIsInstance(i, str) if i == "identifier='%r'" % contour.identifier: idFound = True if i == "in glyph": glyphFound = True self.assertTrue(idFound) self.assertTrue(glyphFound) def test_reprContents_Glyph_noID(self): glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() contour.glyph = glyph value = contour._reprContents() self.assertIsInstance(value, list) glyphFound = False for i in value: self.assertIsInstance(i, str) if i == "in glyph": glyphFound = True self.assertTrue(glyphFound) # ---- # Copy # ---- def test_copyData(self): contour = self.getContour_bounds() contourOther, _ = self.objectGenerator("contour") contourOther.copyData(contour) self.assertEqual( contour.bounds, contourOther.bounds ) # ------- # Parents # ------- def test_parent_glyph_set_glyph(self): glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() contour.glyph = glyph self.assertEqual(glyph, contour.glyph) def test_parent_glyph_set_glyph_None(self): contour = self.getContour_bounds() contour.glyph = None self.assertEqual(None, contour.glyph) def test_parent_glyph_set_already_set(self): glyph, _ = self.objectGenerator("glyph") glyph2, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() contour.glyph = glyph self.assertEqual(glyph, contour.glyph) with self.assertRaises(AssertionError): contour.glyph = glyph2 def test_parent_glyph_get_none(self): contour = self.getContour_bounds() self.assertEqual(None, contour.glyph) def test_parent_glyph_get(self): glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() contour = glyph.appendContour(contour) self.assertEqual(glyph, contour.glyph) def test_parent_font_set(self): font, _ = self.objectGenerator("font") contour = self.getContour_bounds() with self.assertRaises(FontPartsError): contour.font = font def test_parent_font_get_none(self): contour = self.getContour_bounds() self.assertEqual(None, contour.font) def test_parent_font_get(self): font, _ = self.objectGenerator("font") layer, _ = self.objectGenerator("layer") glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() layer.font = font glyph.layer = layer contour = glyph.appendContour(contour) self.assertEqual(font, contour.font) def test_parent_layer_set(self): layer, _ = self.objectGenerator("layer") contour = self.getContour_bounds() with self.assertRaises(FontPartsError): contour.layer = layer def test_parent_layer_get_none(self): contour = self.getContour_bounds() self.assertEqual(None, contour.layer) def test_parent_layer_get(self): layer, _ = self.objectGenerator("layer") glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() glyph.layer = layer contour = glyph.appendContour(contour) self.assertEqual(layer, contour.layer) # ----- # Index # ----- def test_index(self): glyph, _ = self.objectGenerator("glyph") contour1 = glyph.appendContour(self.getContour_bounds()) contour2 = glyph.appendContour(self.getContour_bounds()) contour3 = glyph.appendContour(self.getContour_bounds()) self.assertEqual(contour1.index, 0) self.assertEqual(contour2.index, 1) self.assertEqual(contour3.index, 2) def test_set_index(self): glyph, _ = self.objectGenerator("glyph") contour1 = glyph.appendContour(self.getContour_bounds()) contour2 = glyph.appendContour(self.getContour_bounds()) contour3 = glyph.appendContour(self.getContour_bounds()) contour1.index = 2 self.assertEqual(contour1.index, 2) self.assertEqual(contour2.index, 0) self.assertEqual(contour3.index, 1) contour1.index = 1 self.assertEqual(contour1.index, 1) self.assertEqual(contour2.index, 0) self.assertEqual(contour3.index, 2) # -------------- # Identification # -------------- def test_get_index_no_glyph(self): contour = self.getContour_bounds() self.assertEqual(contour.index, None) def test_get_index_glyph(self): glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() c1 = glyph.appendContour(contour) self.assertEqual(c1.index, 0) c2 = glyph.appendContour(contour) self.assertEqual(c2.index, 1) # ------ # Bounds # ------ def getContour_boundsExtrema(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "line") contour.appendPoint((0, 100), "line") contour.appendPoint((50, 100), "line") contour.appendPoint((117, 100), "offcurve") contour.appendPoint((117, 0), "offcurve") contour.appendPoint((50, 0), "curve") return contour def test_bounds_get(self): contour = self.getContour_bounds() self.assertEqual( contour.bounds, (0, 0, 100, 100) ) def test_bounds_set_float(self): contour = self.getContour_bounds() contour.moveBy((0.5, -0.5)) self.assertEqual( contour.bounds, (0.5, -0.5, 100.5, 99.5) ) def test_bounds_point_not_at_extrema(self): contour = self.getContour_bounds() contour = self.getContour_boundsExtrema() bounds = tuple(int(round(i)) for i in contour.bounds) self.assertEqual( bounds, (0, 0, 100, 100) ) def test_invalid_bounds_set(self): contour = self.getContour_bounds() with self.assertRaises(FontPartsError): contour.bounds = (1, 2, 3, 4) # ---- # Hash # ---- def test_hash_object_self(self): contour_one = self.getContour_bounds() self.assertEqual( hash(contour_one), hash(contour_one) ) def test_hash_object_other(self): contour_one = self.getContour_bounds() contour_two = self.getContour_bounds() self.assertNotEqual( hash(contour_one), hash(contour_two) ) def test_hash_object_self_variable_assignment(self): contour_one = self.getContour_bounds() a = contour_one self.assertEqual( hash(contour_one), hash(a) ) def test_hash_object_other_variable_assignment(self): contour_one = self.getContour_bounds() contour_two = self.getContour_bounds() a = contour_one self.assertNotEqual( hash(contour_two), hash(a) ) def test_is_hashable(self): contour_one = self.getContour_bounds() self.assertTrue( isinstance(contour_one, collections.abc.Hashable) ) # -------- # Equality # -------- def test_object_equal_self(self): contour_one = self.getContour_bounds() self.assertEqual( contour_one, contour_one ) def test_object_not_equal_self(self): contour_one = self.getContour_bounds() contour_two = self.getContour_bounds() self.assertNotEqual( contour_one, contour_two ) def test_object_equal_self_variable_assignment(self): contour_one = self.getContour_bounds() a = contour_one a.moveBy((0.5, -0.5)) self.assertEqual( contour_one, a ) def test_object_not_equal_self_variable_assignment(self): contour_one = self.getContour_bounds() contour_two = self.getContour_bounds() a = contour_one self.assertNotEqual( contour_two, a ) # --------- # Selection # --------- def test_selected_true(self): contour = self.getContour_bounds() try: contour.selected = False except NotImplementedError: return contour.selected = True self.assertEqual( contour.selected, True ) def test_selected_false(self): contour = self.getContour_bounds() try: contour.selected = False except NotImplementedError: return self.assertEqual( contour.selected, False ) def test_selectedSegments_default(self): contour = self.getContour_bounds() segment1 = contour.segments[0] try: segment1.selected = False except NotImplementedError: return self.assertEqual( contour.selectedSegments, () ) def test_selectedSegments_setSubObject(self): contour = self.getContour_bounds() segment1 = contour.segments[0] segment2 = contour.segments[1] try: segment1.selected = False except NotImplementedError: return segment2.selected = True self.assertEqual( contour.selectedSegments == (segment2,), True ) def test_selectedSegments_setFilledList(self): contour = self.getContour_bounds() segment1 = contour.segments[0] segment2 = contour.segments[1] try: segment1.selected = False except NotImplementedError: return contour.selectedSegments = [segment1, segment2] self.assertEqual( contour.selectedSegments, (segment1, segment2) ) def test_selectedSegments_setEmptyList(self): contour = self.getContour_bounds() segment1 = contour.segments[0] try: segment1.selected = True except NotImplementedError: return contour.selectedSegments = [] self.assertEqual( contour.selectedSegments, () ) def test_selectedPoints_default(self): contour = self.getContour_bounds() point1 = contour.points[0] try: point1.selected = False except NotImplementedError: return self.assertEqual( contour.selectedPoints, () ) def test_selectedPoints_setSubObject(self): contour = self.getContour_bounds() point1 = contour.points[0] point2 = contour.points[1] try: point1.selected = False except NotImplementedError: return point2.selected = True self.assertEqual( contour.selectedPoints, (point2,) ) def test_selectedPoints_setFilledList(self): contour = self.getContour_bounds() point1 = contour.points[0] point2 = contour.points[1] try: point1.selected = False except NotImplementedError: return contour.selectedPoints = [point1, point2] self.assertEqual( contour.selectedPoints, (point1, point2) ) def test_selectedPoints_setEmptyList(self): contour = self.getContour_bounds() point1 = contour.points[0] try: point1.selected = True except NotImplementedError: return contour.selectedPoints = [] self.assertEqual( contour.selectedPoints, () ) def test_selectedBPoints_default(self): contour = self.getContour_bounds() bPoint1 = contour.bPoints[0] try: bPoint1.selected = False except NotImplementedError: return self.assertEqual( contour.selectedBPoints, () ) def test_selectedBPoints_setSubObject(self): contour = self.getContour_bounds() bPoint1 = contour.bPoints[0] bPoint2 = contour.bPoints[1] try: bPoint1.selected = False except NotImplementedError: return bPoint2.selected = True self.assertEqual( contour.selectedBPoints, (bPoint2,) ) def test_selectedBPoints_setFilledList(self): contour = self.getContour_bounds() bPoint1 = contour.bPoints[0] bPoint2 = contour.bPoints[1] try: bPoint1.selected = False except NotImplementedError: return contour.selectedBPoints = [bPoint1, bPoint2] self.assertEqual( contour.selectedBPoints, (bPoint1, bPoint2) ) def test_selectedBPoints_setEmptyList(self): contour = self.getContour_bounds() bPoint1 = contour.bPoints[0] try: bPoint1.selected = True except NotImplementedError: return contour.selectedBPoints = [] self.assertEqual( contour.selectedBPoints, () ) # -------- # Segments # -------- def test_segments_offcurves_end(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((84, 0), "curve") contour.appendPoint((0, 0), "line") contour.appendPoint((0, 28), "offcurve") contour.appendPoint((10, 64), "offcurve") contour.appendPoint((46, 64), "curve") contour.appendPoint((76, 64), "offcurve") contour.appendPoint((84, 28), "offcurve") segments = contour.segments self.assertEqual( [segment.type for segment in segments], ["line", "curve", "curve"] ) def test_segments_offcurves_begin_end(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((84, 28), "offcurve") contour.appendPoint((84, 0), "curve") contour.appendPoint((0, 0), "line") contour.appendPoint((0, 28), "offcurve") contour.appendPoint((10, 64), "offcurve") contour.appendPoint((46, 64), "curve") contour.appendPoint((76, 64), "offcurve") segments = contour.segments self.assertEqual( [segment.type for segment in segments], ["line", "curve", "curve"] ) def test_segments_offcurves_begin(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((76, 64), "offcurve") contour.appendPoint((84, 28), "offcurve") contour.appendPoint((84, 0), "curve") contour.appendPoint((0, 0), "line") contour.appendPoint((0, 28), "offcurve") contour.appendPoint((10, 64), "offcurve") contour.appendPoint((46, 64), "curve") segments = contour.segments self.assertEqual( [segment.type for segment in segments], ["line", "curve", "curve"] ) def test_segments_offcurves_middle(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((46, 64), "curve") contour.appendPoint((76, 64), "offcurve") contour.appendPoint((84, 28), "offcurve") contour.appendPoint((84, 0), "curve") contour.appendPoint((0, 0), "line") contour.appendPoint((0, 28), "offcurve") contour.appendPoint((10, 64), "offcurve") segments = contour.segments self.assertEqual( [segment.type for segment in segments], ["curve", "line", "curve"] ) def test_segments_empty(self): contour, _ = self.objectGenerator("contour") segments = contour.segments self.assertEqual(segments, []) def test_segment_insert_open(self): # at index 0 contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((2, 2), "line") contour.appendPoint((3, 3), "line") contour.insertSegment(0, "line", [(1, 1)]) self.assertEqual( [(point.x, point.y) for point in contour.points], [(0, 0), (1, 1), (2, 2), (3, 3)] ) # at index 1 contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((2, 2), "line") contour.appendPoint((3, 3), "line") contour.insertSegment(1, "line", [(1, 1)]) self.assertEqual( [(point.x, point.y) for point in contour.points], [(0, 0), (2, 2), (1, 1), (3, 3)] ) def test_segment_insert_curve_open(self): # at index 0 contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((2, 2), "offcurve") contour.appendPoint((3, 3), "offcurve") contour.appendPoint((4, 4), "curve") contour.appendPoint((5, 5), "line") contour.insertSegment(0, "line", [(1, 1)]) self.assertEqual( [(point.x, point.y) for point in contour.points], [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] ) # at index 1 contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((2, 2), "offcurve") contour.appendPoint((3, 3), "offcurve") contour.appendPoint((4, 4), "curve") contour.appendPoint((5, 5), "line") contour.insertSegment(1, "line", [(1, 1)]) self.assertEqual( [(point.x, point.y) for point in contour.points], [(0, 0), (2, 2), (3, 3), (4, 4), (1, 1), (5, 5)] ) def test_segment_insert_closed(self): # at index 0 contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "line") contour.appendPoint((2, 2), "line") contour.appendPoint((3, 3), "line") contour.insertSegment(0, "line", [(1, 1)]) self.assertEqual( [(point.x, point.y) for point in contour.points], [(0, 0), (1, 1), (2, 2), (3, 3)] ) # at index 1 contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "line") contour.appendPoint((2, 2), "line") contour.appendPoint((3, 3), "line") contour.insertSegment(1, "line", [(1, 1)]) self.assertEqual( [(point.x, point.y) for point in contour.points], [(0, 0), (2, 2), (1, 1), (3, 3)] ) def test_segment_insert_curve_closed(self): # at index 0 contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "line") contour.appendPoint((2, 2), "offcurve") contour.appendPoint((3, 3), "offcurve") contour.appendPoint((4, 4), "curve") contour.appendPoint((5, 5), "line") contour.insertSegment(0, "line", [(1, 1)]) self.assertEqual( [(point.x, point.y) for point in contour.points], [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] ) # at index 1 contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "line") contour.appendPoint((2, 2), "offcurve") contour.appendPoint((3, 3), "offcurve") contour.appendPoint((4, 4), "curve") contour.appendPoint((5, 5), "line") contour.insertSegment(1, "line", [(1, 1)]) self.assertEqual( [(point.x, point.y) for point in contour.points], [(0, 0), (2, 2), (3, 3), (4, 4), (1, 1), (5, 5)] ) def test_setStartSegment(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((50, 0), "curve") contour.appendPoint((75, 0), "offcurve") contour.appendPoint((100, 25), "offcurve") contour.appendPoint((100, 50), "curve") contour.appendPoint((100, 75), "offcurve") contour.appendPoint((75, 100), "offcurve") contour.appendPoint((50, 100), "curve") contour.appendPoint((25, 100), "offcurve") contour.appendPoint((0, 75), "offcurve") contour.appendPoint((0, 50), "curve") contour.appendPoint((0, 25), "offcurve") contour.appendPoint((25, 0), "offcurve") contour.setStartSegment(1) self.assertEqual(contour.points[0].type, "curve") self.assertEqual((contour.points[0].x, contour.points[0].y), (100, 50)) # ------ # points # ------ def test_setStartPoint(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "line") contour.appendPoint((1, 1), "line") contour.appendPoint((2, 2), "line") contour.appendPoint((3, 3), "line") contour.setStartPoint(2) self.assertEqual( [(point.x, point.y) for point in contour.points], [(2, 2), (3, 3), (0, 0), (1, 1)] ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_deprecated.py000066400000000000000000002246501477533125200253350ustar00rootroot00000000000000import unittest from fontParts.base.deprecated import RemovedError class TestDeprecated(unittest.TestCase): # ---- # Font # ---- def getFont_glyphs(self): font, _ = self.objectGenerator("font") for name in "ABCD": font.newGlyph(name) return font def test_font_removed_getParent(self): font, _ = self.objectGenerator("font") with self.assertRaises(RemovedError): font.getParent() def test_font_removed_generateGlyph(self): font, _ = self.objectGenerator("font") with self.assertRaises(RemovedError): font.generateGlyph() def test_font_removed_compileGlyph(self): font, _ = self.objectGenerator("font") with self.assertRaises(RemovedError): font.compileGlyph() def test_font_removed_getGlyphNameToFileNameFunc(self): font, _ = self.objectGenerator("font") with self.assertRaises(RemovedError): font.getGlyphNameToFileNameFunc() def test_font_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. font, _ = self.objectGenerator("font") with self.assertWarnsRegex(DeprecationWarning, "Font.changed()"): font.update() def test_font_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. font, _ = self.objectGenerator("font") with self.assertWarnsRegex(DeprecationWarning, "Font.changed()"): font.setChanged() def test_font_removed_setParent(self): font, _ = self.objectGenerator("font") with self.assertRaises(RemovedError): font.setParent(font) def test_font_deprecated__fileName(self): font, _ = self.objectGenerator("font") with self.assertWarnsRegex(DeprecationWarning, "fileName"): font._get_fileName() self.assertEqual(font._get_fileName(), font.path) def test_font_deprecated_fileName(self): font, _ = self.objectGenerator("font") with self.assertWarnsRegex(DeprecationWarning, "fileName"): font.fileName self.assertEqual(font.fileName, font.path) def test_font_deprecated_getWidth(self): font, _ = self.objectGenerator("font") glyph = font.newGlyph("Test") glyph.width = 200 with self.assertWarnsRegex(DeprecationWarning, "Font.getWidth()"): font.getWidth("Test") self.assertEqual(font.getWidth("Test"), font["Test"].width) def test_font_deprecated_getGlyph(self): font, _ = self.objectGenerator("font") font.newGlyph("Test") with self.assertWarnsRegex(DeprecationWarning, "Font.getGlyph()"): font.getGlyph("Test") self.assertEqual(font.getGlyph("Test"), font["Test"]) def test_font_deprecated__get_selection(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return glyph1 = font["A"] glyph2 = font["B"] glyph1.selected = True glyph2.selected = True with self.assertWarnsRegex(DeprecationWarning, "Font.selectedGlyphNames"): font._get_selection() self.assertEqual(font._get_selection(), font.selectedGlyphNames) def test_font_deprecated__set_selection(self): font1 = self.getFont_glyphs() font2 = self.getFont_glyphs() with self.assertWarnsRegex(DeprecationWarning, "Font.selectedGlyphNames"): font1._set_selection(["A", "B"]) font2.selectedGlyphNames = ["A", "B"] self.assertEqual(font1.selectedGlyphNames, font2.selectedGlyphNames) def test_font_deprecated_selection_set(self): font1 = self.getFont_glyphs() font2 = self.getFont_glyphs() with self.assertWarnsRegex(DeprecationWarning, "Font.selectedGlyphNames"): font1.selection = ["A", "B"] font2.selectedGlyphNames = ["A", "B"] self.assertEqual(font1.selectedGlyphNames, font2.selectedGlyphNames) def test_font_deprecated_selection_get(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return glyph1 = font["A"] glyph2 = font["B"] glyph1.selected = True glyph2.selected = True with self.assertWarnsRegex(DeprecationWarning, "Font.selectedGlyphNames"): font.selection self.assertEqual(font.selection, font.selectedGlyphNames) # ------ # Anchor # ------ def getAnchor(self): glyph, _ = self.objectGenerator("glyph") glyph.appendAnchor("anchor 0", (10, 20)) return glyph.anchors[0] def test_anchor_deprecated__generateIdentifer(self): anchor, _ = self.objectGenerator("anchor") with self.assertWarnsRegex(DeprecationWarning, "Anchor._generateIdentifier()"): anchor._generateIdentifier() self.assertEqual( anchor._generateIdentifier(), anchor._getIdentifier() ) def test_anchor_deprecated_generateIdentifer(self): anchor, _ = self.objectGenerator("anchor") with self.assertWarnsRegex(DeprecationWarning, "Anchor.generateIdentifier()"): anchor.generateIdentifier() self.assertEqual( anchor.generateIdentifier(), anchor.getIdentifier() ) def test_anchor_deprecated_getParent(self): anchor = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.getParent()"): anchor.getParent() self.assertEqual( anchor.getParent(), anchor.glyph ) def test_anchor_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. anchor, _ = self.objectGenerator("anchor") with self.assertWarnsRegex(DeprecationWarning, "Anchor.changed()"): anchor.update() def test_anchor_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. anchor, _ = self.objectGenerator("anchor") with self.assertWarnsRegex(DeprecationWarning, "Anchor.changed()"): anchor.setChanged() def test_anchor_removed_setParent(self): anchor = self.getAnchor() glyph = anchor.glyph with self.assertRaises(RemovedError): anchor.setParent(glyph) def test_anchor_removed_draw(self): anchor = self.getAnchor() pen = anchor.glyph.getPen() with self.assertRaises(RemovedError): anchor.draw(pen) def test_anchor_removed_drawPoints(self): anchor = self.getAnchor() pen = anchor.glyph.getPen() with self.assertRaises(RemovedError): anchor.drawPoints(pen) def test_anchor_deprecated_move(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.move()"): anchor1.move((0, 20)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.moveBy((0, 20)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_translate(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.translate()"): anchor1.translate((0, 20)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.moveBy((0, 20)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_scale_no_center(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.scale()"): anchor1.scale((-2)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.scaleBy((-2)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_scale_center(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.scale()"): anchor1.scale((-2, 3), center=(1, 2)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_rotate_no_offset(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.rotate()"): anchor1.rotate(45) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.rotateBy(45) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_rotate_offset(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.rotate()"): anchor1.rotate(45, offset=(1, 2)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.rotateBy(45, origin=(1, 2)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_transform(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.transform()"): anchor1.transform((2, 0, 0, 3, -3, 2)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_skew_no_offset_one_value(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.skew()"): anchor1.skew(100) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.skewBy(100) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_skew_no_offset_two_values(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.skew()"): anchor1.skew((100, 200)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.skewBy((100, 200)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_skew_offset_one_value(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.skew()"): anchor1.skew(100, offset=(1, 2)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.skewBy(100, origin=(1, 2)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) def test_anchor_deprecated_skew_offset_two_values(self): anchor1 = self.getAnchor() anchor2 = self.getAnchor() with self.assertWarnsRegex(DeprecationWarning, "Anchor.skew()"): anchor1.skew((100, 200), offset=(1, 2)) self.assertNotEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) anchor2.skewBy((100, 200), origin=(1, 2)) self.assertEqual((anchor1.x, anchor1.y), (anchor2.x, anchor2.y)) # ----- # Layer # ----- def test_layer_deprecated_getParent(self): font, _ = self.objectGenerator("font") for name in "ABCD": font.newLayer("layer " + name) layer = font.layers[0] with self.assertWarnsRegex(DeprecationWarning, "Layer.font"): layer.getParent() self.assertEqual(layer.getParent(), layer.font) def test_layer_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. layer, _ = self.objectGenerator("layer") with self.assertWarnsRegex(DeprecationWarning, "Layer.changed()"): layer.update() def test_layer_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. layer, _ = self.objectGenerator("layer") with self.assertWarnsRegex(DeprecationWarning, "Layer.changed()"): layer.setChanged() def test_layer_removed_setParent(self): font, _ = self.objectGenerator("font") for name in "ABCD": font.newLayer("layer " + name) layer = font.layers[0] with self.assertRaises(RemovedError): layer.setParent(font) # -------- # Features # -------- def test_features_deprecated_getParent(self): font, _ = self.objectGenerator("font") features = font.features features.text = "# Test" with self.assertWarnsRegex(DeprecationWarning, "Features.font"): features.getParent() self.assertEqual(features.getParent(), features.font) def test_features_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. features, _ = self.objectGenerator("features") with self.assertWarnsRegex(DeprecationWarning, "Features.changed()"): features.update() def test_features_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. features, _ = self.objectGenerator("features") with self.assertWarnsRegex(DeprecationWarning, "Features.changed()"): features.setChanged() def test_feature_removed_setParent(self): font, _ = self.objectGenerator("font") features = font.features features.text = "# Test" with self.assertRaises(RemovedError): features.setParent(font) def test_feature_removed_round(self): feature, _ = self.objectGenerator("features") with self.assertRaises(RemovedError): feature.round() # ----- # Image # ----- def getImage_glyph(self): from fontParts.test.test_image import testImageData glyph, _ = self.objectGenerator("glyph") glyph.addImage(data=testImageData) image = glyph.image return image def test_image_deprecated_getParent(self): image = self.getImage_glyph() with self.assertWarnsRegex(DeprecationWarning, "Image.glyph"): image.getParent() self.assertEqual(image.getParent(), image.glyph) def test_image_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. image, _ = self.objectGenerator("image") with self.assertWarnsRegex(DeprecationWarning, "Image.changed()"): image.update() def test_image_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. image, _ = self.objectGenerator("image") with self.assertWarnsRegex(DeprecationWarning, "Image.changed()"): image.setChanged() def test_image_removed_setParent(self): glyph, _ = self.objectGenerator("glyph") image = self.getImage_glyph() with self.assertRaises(RemovedError): image.setParent(glyph) # ---- # Info # ---- def test_info_deprecated_getParent(self): font, _ = self.objectGenerator("font") info = font.info info.unitsPerEm = 1000 with self.assertWarnsRegex(DeprecationWarning, "Info.font"): info.getParent() self.assertEqual(info.getParent(), info.font) def test_info_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. info, _ = self.objectGenerator("info") with self.assertWarnsRegex(DeprecationWarning, "Info.changed()"): info.setChanged() def test_info_removed_setParent(self): font, _ = self.objectGenerator("font") info, _ = self.objectGenerator("info") info.unitsPerEm = 1000 with self.assertRaises(RemovedError): info.setParent(font) # ------- # Kerning # ------- def getKerning_generic(self): font, _ = self.objectGenerator("font") groups = font.groups groups["public.kern1.X"] = ["A", "B", "C"] groups["public.kern2.X"] = ["A", "B", "C"] kerning = font.kerning kerning.update({ ("public.kern1.X", "public.kern2.X"): 100, ("B", "public.kern2.X"): 101, ("public.kern1.X", "B"): 102, ("A", "A"): 103, }) return kerning def test_kerning_removed_setParent(self): font, _ = self.objectGenerator("font") kerning, _ = self.objectGenerator("kerning") with self.assertRaises(RemovedError): kerning.setParent(font) def test_kerning_removed_swapNames(self): kerning = self.getKerning_generic() swap = {"B": "C"} with self.assertRaises(RemovedError): kerning.swapNames(swap) def test_kerning_removed_getLeft(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.getLeft("B") def test_kerning_removed_getRight(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.getRight("B") def test_kerning_removed_getExtremes(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.getExtremes() def test_kerning_removed_add(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.add(10) def test_kerning_removed_minimize(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.minimize() def test_kerning_removed_importAFM(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.importAFM("fake/path") def test_kerning_removed_getAverage(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.getAverage() def test_kerning_removed_combine(self): kerning = self.getKerning_generic() one = {("A", "v"): -10} two = {("v", "A"): -10} with self.assertRaises(RemovedError): kerning.combine([one, two]) def test_kerning_removed_eliminate(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.eliminate(leftGlyphsToEliminate=["A"]) def test_kerning_removed_occurrenceCount(self): kerning = self.getKerning_generic() with self.assertRaises(RemovedError): kerning.occurrenceCount(["A"]) def test_kerning_removed_implodeClasses(self): kerning = self.getKerning_generic() classes = {"group": ["a", "v"]} with self.assertRaises(RemovedError): kerning.implodeClasses(leftClassDict=classes) def test_kerning_removed_explodeClasses(self): kerning = self.getKerning_generic() classes = {"group": ["a", "v"]} with self.assertRaises(RemovedError): kerning.explodeClasses(leftClassDict=classes) def test_kerning_removed_setChanged(self): kerning = self.getKerning_generic() # As changed() is defined by the environment, only test if a Warning is issued. with self.assertWarnsRegex(DeprecationWarning, "Kerning.changed()"): kerning.setChanged() def test_kerning_removed_getParent(self): kerning = self.getKerning_generic() with self.assertWarnsRegex(DeprecationWarning, "Kerning.font"): kerning.getParent() self.assertEqual(kerning.getParent(), kerning.font) # ------ # Groups # ------ def test_groups_deprecated_getParent(self): font, _ = self.objectGenerator("font") groups = font.groups groups.update({ "group 1": ["A", "B", "C"], "group 2": ["x", "y", "z"], "group 3": [], "group 4": ["A"] }) with self.assertWarnsRegex(DeprecationWarning, "Groups.font"): groups.getParent() self.assertEqual(groups.getParent(), groups.font) def test_groups_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. groups, _ = self.objectGenerator("groups") with self.assertWarnsRegex(DeprecationWarning, "Groups.changed()"): groups.setChanged() def test_groups_removed_setParent(self): font, _ = self.objectGenerator("font") groups, _ = self.objectGenerator("groups") groups.update({ "group 1": ["A", "B", "C"], "group 2": ["x", "y", "z"], "group 3": [], "group 4": ["A"] }) with self.assertRaises(RemovedError): groups.setParent(font) # --- # Lib # --- def test_lib_deprecated_getParent_font(self): font, _ = self.objectGenerator("font") lib = font.lib lib.update({ "key 1": ["A", "B", "C"], "key 2": "x", "key 3": [], "key 4": 20 }) with self.assertWarnsRegex(DeprecationWarning, "Lib.font"): lib.getParent() self.assertEqual(lib.getParent(), lib.font) def test_lib_deprecated_getParent_glyph(self): font, _ = self.objectGenerator("font") glyph = font.newGlyph("Test") lib = glyph.lib lib.update({ "key 1": ["A", "B", "C"], "key 2": "x", "key 3": [], "key 4": 20 }) with self.assertWarnsRegex(DeprecationWarning, "Lib.glyph"): lib.getParent() self.assertEqual(lib.getParent(), lib.glyph) def test_lib_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. lib, _ = self.objectGenerator("lib") with self.assertWarnsRegex(DeprecationWarning, "Lib.changed()"): lib.setChanged() def test_lib_removed_setParent_font(self): font, _ = self.objectGenerator("font") lib, _ = self.objectGenerator("lib") lib.update({ "key 1": ["A", "B", "C"], "key 2": "x", "key 3": [], "key 4": 20 }) with self.assertRaises(RemovedError): lib.setParent(font) def test_lib_removed_setParent_glyph(self): glyph, _ = self.objectGenerator("glyph") lib, _ = self.objectGenerator("lib") lib.update({ "key 1": ["A", "B", "C"], "key 2": "x", "key 3": [], "key 4": 20 }) with self.assertRaises(RemovedError): lib.setParent(glyph) # --------- # Guideline # --------- def getGuideline_generic(self): guideline, _ = self.objectGenerator("guideline") guideline.x = 1 guideline.y = 2 guideline.angle = 90 return guideline def getGuideline_transform(self): guideline = self.getGuideline_generic() guideline.angle = 45.0 return guideline def test_guideline_deprecated__generateIdentifer(self): guideline = self.getGuideline_generic() with self.assertWarnsRegex(DeprecationWarning, "Guideline._getIdentifier()"): guideline._generateIdentifier() self.assertEqual(guideline._generateIdentifier(), guideline._getIdentifier()) def test_guideline_deprecated_generateIdentifer(self): guideline = self.getGuideline_generic() with self.assertWarnsRegex(DeprecationWarning, "Guideline.getIdentifier()"): guideline.generateIdentifier() self.assertEqual(guideline.generateIdentifier(), guideline.getIdentifier()) def test_guideline_deprecated_getParent_glyph(self): glyph, _ = self.objectGenerator("glyph") guideline = self.getGuideline_generic() guideline.glyph = glyph with self.assertWarnsRegex(DeprecationWarning, "Guideline.glyph"): guideline.getParent() self.assertEqual(guideline.getParent(), guideline.glyph) def test_guideline_deprecated_getParent_font(self): font, _ = self.objectGenerator("font") guideline = self.getGuideline_generic() guideline.font = font with self.assertWarnsRegex(DeprecationWarning, "Guideline.font"): guideline.getParent() self.assertEqual(guideline.getParent(), guideline.font) def test_guideline_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. guideline, _ = self.objectGenerator("guideline") with self.assertWarnsRegex(DeprecationWarning, "Guideline.changed()"): guideline.update() def test_guideline_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. guideline, _ = self.objectGenerator("guideline") with self.assertWarnsRegex(DeprecationWarning, "Guideline.changed()"): guideline.setChanged() def test_guideline_removed_setParent(self): font, _ = self.objectGenerator("font") guideline = self.getGuideline_generic() with self.assertRaises(RemovedError): guideline.setParent(font) def test_guideline_deprecated_move(self): guideline1, _ = self.objectGenerator("guideline") guideline2, _ = self.objectGenerator("guideline") with self.assertWarnsRegex(DeprecationWarning, "Guideline.move()"): guideline1.move((0, 20)) guideline2.moveBy((0, 20)) self.assertEqual(guideline1.y, guideline2.y) def test_guideline_deprecated_translate(self): guideline1, _ = self.objectGenerator("guideline") guideline2, _ = self.objectGenerator("guideline") with self.assertWarnsRegex(DeprecationWarning, "Guideline.translate()"): guideline1.translate((0, 20)) guideline2.moveBy((0, 20)) self.assertEqual(guideline1.y, guideline2.y) def test_guideline_deprecated_scale_no_center(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.scale()"): guideline.scale((-2)) self.assertEqual(guideline.x, -2) self.assertEqual(guideline.y, -4) self.assertAlmostEqual(guideline.angle, 225.000, places=3) def test_guideline_deprecated_scale_center(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.scale()"): guideline.scale((-2, 3), center=(1, 2)) self.assertEqual(guideline.x, 1) self.assertEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 123.690, places=3) def test_guideline_deprecated_rotate_no_offset(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.rotate()"): guideline.rotate(45) self.assertAlmostEqual(guideline.x, -0.707, places=3) self.assertAlmostEqual(guideline.y, 2.121, places=3) self.assertAlmostEqual(guideline.angle, 0.000, places=3) def test_guideline_deprecated_rotate_offset(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.rotate()"): guideline.rotate(45, offset=(1, 2)) self.assertAlmostEqual(guideline.x, 1) self.assertAlmostEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 0.000, places=3) def test_guideline_deprecated_transform(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.transform()"): guideline.transform((2, 0, 0, 3, -3, 2)) self.assertEqual(guideline.x, -1) self.assertEqual(guideline.y, 8) self.assertAlmostEqual(guideline.angle, 56.310, places=3) def test_guideline_deprecated_skew_no_offset_one_value(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.skew()"): guideline.skew(100) self.assertAlmostEqual(guideline.x, -10.343, places=3) self.assertEqual(guideline.y, 2.0) self.assertAlmostEqual(guideline.angle, 8.525, places=3) def test_guideline_deprecated_skew_no_offset_two_values(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.skew()"): guideline.skew((100, 200)) self.assertAlmostEqual(guideline.x, -10.343, places=3) self.assertAlmostEqual(guideline.y, 2.364, places=3) self.assertAlmostEqual(guideline.angle, 5.446, places=3) def test_guideline_deprecated_skew_offset_one_value(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.skew()"): guideline.skew(100, offset=(1, 2)) self.assertEqual(guideline.x, 1) self.assertEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 8.525, places=3) def test_guideline_deprecated_skew_offset_two_values(self): guideline = self.getGuideline_transform() with self.assertWarnsRegex(DeprecationWarning, "Guideline.skew()"): guideline.skew((100, 200), offset=(1, 2)) self.assertEqual(guideline.x, 1) self.assertEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 5.446, places=3) # ----- # Glyph # ----- def getGlyph_generic(self): glyph, _ = self.objectGenerator("glyph") glyph.name = "Test Glyph 1" glyph.unicode = int(ord("X")) glyph.width = 250 glyph.height = 750 pen = glyph.getPen() pen.moveTo((100, -10)) pen.lineTo((100, 100)) pen.lineTo((200, 100)) pen.lineTo((200, 0)) pen.closePath() pen.moveTo((110, 10)) pen.lineTo((110, 90)) pen.lineTo((190, 90)) pen.lineTo((190, 10)) pen.closePath() glyph.appendAnchor("Test Anchor 1", (1, 2)) glyph.appendAnchor("Test Anchor 2", (3, 4)) glyph.appendGuideline((1, 2), 0, "Test Guideline 1") glyph.appendGuideline((3, 4), 90, "Test Guideline 2") return glyph def test_glyph_removed_center(self): glyph = self.getGlyph_generic() with self.assertRaisesRegex(RemovedError, "center()"): glyph.center() def test_glyph_removed_clearVGuides(self): glyph = self.getGlyph_generic() with self.assertRaisesRegex(RemovedError, "clearGuidelines()"): glyph.clearVGuides() def test_glyph_removed_clearHGuides(self): glyph = self.getGlyph_generic() with self.assertRaisesRegex(RemovedError, "clearGuidelines()"): glyph.clearHGuides() def test_glyph_removed_setParent(self): font, _ = self.objectGenerator("font") glyph = self.getGlyph_generic() with self.assertRaisesRegex(RemovedError, "setParent()"): glyph.setParent(font) def test_glyph_deprecated_get_mark(self): glyph = self.getGlyph_generic() glyph.markColor = (1, 0, 0, 1) with self.assertWarnsRegex(DeprecationWarning, "Glyph.markColor"): glyph._get_mark() self.assertEqual(glyph._get_mark(), glyph.markColor) def test_glyph_deprecated_set_mark(self): glyph = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.markColor"): glyph._set_mark((1, 0, 0, 1)) self.assertEqual((1, 0, 0, 1), glyph.markColor) def test_glyph_deprecated_mark(self): glyph = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.markColor"): glyph.mark = (1, 0, 0, 1) with self.assertWarnsRegex(DeprecationWarning, "Glyph.markColor"): mark = glyph.mark self.assertEqual((1, 0, 0, 1), mark) def test_glyph_deprecated__get_box(self): glyph = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.bounds"): glyph._get_box() self.assertEqual(glyph._get_box(), glyph.bounds) def test_glyph_deprecated_box(self): glyph = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.bounds"): box = glyph.box self.assertEqual(box, (100, -10, 200, 100)) def test_glyph_deprecated_getAnchors(self): glyph = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.anchors"): anchors = glyph.getAnchors() self.assertEqual(anchors, glyph.anchors) def test_glyph_deprecated_getComponents(self): glyph = self.getGlyph_generic() glyph.appendComponent("component 1") with self.assertWarnsRegex(DeprecationWarning, "Glyph.components"): components = glyph.getComponents() self.assertEqual(components, glyph.components) def test_glyph_deprecated_getParent(self): font, _ = self.objectGenerator("font") layer = font.layers[0] glyph = layer.newGlyph("A") with self.assertWarnsRegex(DeprecationWarning, "Glyph.font"): parent = glyph.getParent() self.assertEqual(parent, glyph.font) def test_glyph_deprecated_writeGlyphToString(self): glyph = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.dumpToGLIF()"): data = glyph.writeGlyphToString() self.assertEqual(data, glyph.dumpToGLIF()) def test_glyph_deprecated_readGlyphToString(self): glyph = self.getGlyph_generic() glyph2, _ = self.objectGenerator("glyph") data = glyph.dumpToGLIF() with self.assertWarnsRegex(DeprecationWarning, "Glyph.loadFromGLIF()"): glyph2.readGlyphFromString(data) self.assertEqual(glyph.bounds, glyph2.bounds) self.assertEqual(len(glyph), len(glyph2)) def test_glyph_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. glyph, _ = self.objectGenerator("glyph") with self.assertWarnsRegex(DeprecationWarning, "Glyph.changed()"): glyph.update() def test_glyph_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. glyph, _ = self.objectGenerator("glyph") with self.assertWarnsRegex(DeprecationWarning, "Glyph.changed()"): glyph.setChanged() def test_glyph_deprecated_move(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.move()"): glyph1.move((0, 20)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.moveBy((0, 20)) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_translate(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.translate()"): glyph1.translate((0, 20)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.moveBy((0, 20)) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_scale_no_center(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.scale()"): glyph1.scale((-2)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.scaleBy((-2)) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_scale_center(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.scale()"): glyph1.scale((-2, 3), center=(1, 2)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_rotate_no_offset(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.rotate()"): glyph1.rotate(45) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.rotateBy(45) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_rotate_offset(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.rotate()"): glyph1.rotate(45, offset=(1, 2)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.rotateBy(45, origin=(1, 2)) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_transform(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.transform()"): glyph1.transform((2, 0, 0, 3, -3, 2)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_skew_no_offset_one_value(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.skew()"): glyph1.skew(100) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.skewBy(100) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_skew_no_offset_two_values(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.skew()"): glyph1.skew((100, 200)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.skewBy((100, 200)) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_skew_offset_one_value(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.skew()"): glyph1.skew(100, offset=(1, 2)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.skewBy(100, origin=(1, 2)) self.assertEqual(glyph1.bounds, glyph2.bounds) def test_glyph_deprecated_skew_offset_two_values(self): glyph1 = self.getGlyph_generic() glyph2 = self.getGlyph_generic() with self.assertWarnsRegex(DeprecationWarning, "Glyph.skew()"): glyph1.skew((100, 200), offset=(1, 2)) self.assertNotEqual(glyph1.bounds, glyph2.bounds) glyph2.skewBy((100, 200), origin=(1, 2)) self.assertEqual(glyph1.bounds, glyph2.bounds) # ------- # Contour # ------- def getContour_bounds(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "line") contour.appendPoint((0, 100), "line") contour.appendPoint((100, 100), "line") contour.appendPoint((100, 0), "line") return contour def test_contour_removed_setParent(self): glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() with self.assertRaisesRegex(RemovedError, "setParent()"): contour.setParent(glyph) def test_contour_deprecated__get_box(self): contour = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.bounds"): box = contour._get_box() self.assertEqual(box, contour.bounds) def test_contour_deprecated_box(self): contour = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.bounds"): box = contour.box self.assertEqual(box, contour.bounds) def test_contour_deprecated_reverseContour(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() self.assertEqual(contour1.clockwise, contour2.clockwise) with self.assertWarnsRegex(DeprecationWarning, "Contour.reverse()"): contour1.reverseContour() self.assertNotEqual(contour1.clockwise, contour2.clockwise) contour2.reverse() self.assertEqual(contour1.clockwise, contour2.clockwise) def test_contour_deprecated__generateIdentifer(self): contour, _ = self.objectGenerator("contour") with self.assertWarnsRegex(DeprecationWarning, "Contour._generateIdentifier()"): i = contour._generateIdentifier() self.assertEqual(i, contour._getIdentifier()) def test_contour_deprecated_generateIdentifer(self): contour, _ = self.objectGenerator("contour") with self.assertWarnsRegex(DeprecationWarning, "Contour.generateIdentifier()"): i = contour.generateIdentifier() self.assertEqual(i, contour.getIdentifier()) def test_contour_deprecated__generateIdentiferforPoint(self): contour = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour._generateIdentifierforPoint()"): i = contour._generateIdentifierforPoint(contour[0][0]) self.assertEqual(i, contour._getIdentifierforPoint(contour[0][0])) def test_contour_deprecated_generateIdentiferForPoint(self): contour = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.generateIdentifierforPoint()"): i = contour.generateIdentifierforPoint(contour[0][0]) self.assertEqual(i, contour.getIdentifierForPoint(contour[0][0])) def test_contour_deprecated_getParent(self): glyph, _ = self.objectGenerator("glyph") contour = self.getContour_bounds() contour.glyph = glyph with self.assertWarnsRegex(DeprecationWarning, "Contour.glyph"): p = contour.getParent() self.assertEqual(p, contour.glyph) def test_contour_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. contour, _ = self.objectGenerator("contour") with self.assertWarnsRegex(DeprecationWarning, "Contour.changed()"): contour.update() def test_contour_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. contour, _ = self.objectGenerator("contour") with self.assertWarnsRegex(DeprecationWarning, "Contour.changed()"): contour.setChanged() def test_contour_deprecated_move(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.move()"): contour1.move((0, 20)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.moveBy((0, 20)) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_translate(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.translate()"): contour1.translate((0, 20)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.moveBy((0, 20)) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_scale_no_center(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.scale()"): contour1.scale((-2)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.scaleBy((-2)) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_scale_center(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.scale()"): contour1.scale((-2, 3), center=(1, 2)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_rotate_no_offset(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.rotate()"): contour1.rotate(45) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.rotateBy(45) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_rotate_offset(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.rotate()"): contour1.rotate(45, offset=(1, 2)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.rotateBy(45, origin=(1, 2)) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_transform(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.transform()"): contour1.transform((2, 0, 0, 3, -3, 2)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_skew_no_offset_one_value(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.skew()"): contour1.skew(100) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.skewBy(100) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_skew_no_offset_two_values(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.skew()"): contour1.skew((100, 200)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.skewBy((100, 200)) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_skew_offset_one_value(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.skew()"): contour1.skew(100, offset=(1, 2)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.skewBy(100, origin=(1, 2)) self.assertEqual(contour1.bounds, contour2.bounds) def test_contour_deprecated_skew_offset_two_values(self): contour1 = self.getContour_bounds() contour2 = self.getContour_bounds() with self.assertWarnsRegex(DeprecationWarning, "Contour.skew()"): contour1.skew((100, 200), offset=(1, 2)) self.assertNotEqual(contour1.bounds, contour2.bounds) contour2.skewBy((100, 200), origin=(1, 2)) self.assertEqual(contour1.bounds, contour2.bounds) # ------- # Segment # ------- def getSegment(self): contour, unrequested = self.objectGenerator("contour") unrequested.append(contour) contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") segment = contour[1] return segment def test_segment_removed_insertPoint(self): segment = self.getSegment() with self.assertRaisesRegex(RemovedError, "insertPoint()"): segment.insertPoint(None) def test_segment_removed_removePoint(self): segment = self.getSegment() with self.assertRaisesRegex(RemovedError, "removePoint()"): segment.removePoint(None) def test_segment_removed_setParent(self): segment = self.getSegment() with self.assertRaisesRegex(RemovedError, "setParent()"): segment.setParent(None) def test_segment_deprecated_getParent(self): segment = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.contour"): c = segment.getParent() self.assertEqual(c, segment.contour) def test_segment_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. segment = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.changed()"): segment.update() def test_segment_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. segment = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.changed()"): segment.setChanged() def test_segment_deprecated_move(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.move()"): segment1.move((0, 20)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.moveBy((0, 20)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_translate(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.translate()"): segment1.translate((0, 20)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.moveBy((0, 20)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_scale_no_center(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.scale()"): segment1.scale((-2)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.scaleBy((-2)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_scale_center(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.scale()"): segment1.scale((-2, 3), center=(1, 2)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.scaleBy((-2, 3), origin=(1, 2)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_rotate_no_offset(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.rotate()"): segment1.rotate(45) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.rotateBy(45) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_rotate_offset(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.rotate()"): segment1.rotate(45, offset=(1, 2)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.rotateBy(45, origin=(1, 2)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_transform(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.transform()"): segment1.transform((2, 0, 0, 3, -3, 2)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.transformBy((2, 0, 0, 3, -3, 2)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_skew_no_offset_one_value(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.skew()"): segment1.skew(100) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.skewBy(100) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_skew_no_offset_two_values(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.skew()"): segment1.skew((100, 200)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.skewBy((100, 200)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_skew_offset_one_value(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.skew()"): segment1.skew(100, offset=(1, 2)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.skewBy(100, origin=(1, 2)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) def test_segment_deprecated_skew_offset_two_values(self): segment1 = self.getSegment() segment2 = self.getSegment() with self.assertWarnsRegex(DeprecationWarning, "Segment.skew()"): segment1.skew((100, 200), offset=(1, 2)) coordinates1 = tuple((point.x, point.y) for point in segment1.points) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertNotEqual(coordinates1, coordinates2) segment2.skewBy((100, 200), origin=(1, 2)) coordinates2 = tuple((point.x, point.y) for point in segment2.points) self.assertEqual(coordinates1, coordinates2) # --------- # Component # --------- def getComponent(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("A") pen = glyph.getPen() pen.moveTo((0, 0)) pen.lineTo((0, 100)) pen.lineTo((100, 100)) pen.lineTo((100, 0)) pen.closePath() for i, point in enumerate(glyph[0].points): point.name = "point %d" % i glyph = layer.newGlyph("B") component = glyph.appendComponent("A") component.transformation = (1, 2, 3, 4, 5, 6) return component def test_component_removed_setParent(self): component = self.getComponent() with self.assertRaisesRegex(RemovedError, "setParent()"): component.setParent(None) def test_component_deprecated__get_box(self): component = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.bounds"): box = component._get_box() self.assertEqual(box, component.bounds) def test_component_deprecated_box(self): component = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.bounds"): box = component.box self.assertEqual(box, component.bounds) def test_component_deprecated__generateIdentifier(self): component = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component._getIdentifier()"): i = component._generateIdentifier() self.assertEqual(i, component._getIdentifier()) def test_component_deprecated_generateIdentifier(self): component = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.getIdentifier()"): i = component.generateIdentifier() self.assertEqual(i, component.getIdentifier()) def test_component_deprecated_getParent(self): component = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.glyph"): p = component.getParent() self.assertEqual(p, component.glyph) def test_component_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. component = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.changed()"): component.update() def test_component_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. component = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.changed()"): component.setChanged() def test_component_deprecated_move(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.move()"): component1.move((0, 20)) self.assertNotEqual(component1.bounds, component2.bounds) component2.moveBy((0, 20)) self.assertEqual(component1.bounds, component2.bounds) def test_component_deprecated_translate(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.translate()"): component1.translate((0, 20)) self.assertNotEqual(component1.bounds, component2.bounds) component2.moveBy((0, 20)) self.assertEqual(component1.bounds, component2.bounds) def test_component_deprecated_rotate_no_offset(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.rotate()"): component1.rotate(45) self.assertNotEqual(component1.bounds, component2.bounds) component2.rotateBy(45) self.assertEqual(component1.bounds, component2.bounds) def test_component_deprecated_rotate_offset(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.rotate()"): component1.rotate(45, offset=(1, 2)) self.assertNotEqual(component1.bounds, component2.bounds) component2.rotateBy(45, origin=(1, 2)) self.assertEqual(component1.bounds, component2.bounds) def test_component_deprecated_transform(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.transform()"): component1.transform((2, 0, 0, 3, -3, 2)) self.assertNotEqual(component1.bounds, component2.bounds) component2.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual(component1.bounds, component2.bounds) def test_component_deprecated_skew_no_offset_one_value(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.skew()"): component1.skew(100) self.assertNotEqual(component1.bounds, component2.bounds) component2.skewBy(100) self.assertEqual(component1.bounds, component2.bounds) def test_component_deprecated_skew_no_offset_two_values(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.skew()"): component1.skew((100, 200)) self.assertNotEqual(component1.bounds, component2.bounds) component2.skewBy((100, 200)) self.assertEqual(component1.bounds, component2.bounds) def test_component_deprecated_skew_offset_one_value(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.skew()"): component1.skew(100, offset=(1, 2)) self.assertNotEqual(component1.bounds, component2.bounds) component2.skewBy(100, origin=(1, 2)) self.assertEqual(component1.bounds, component2.bounds) def test_component_deprecated_skew_offset_two_values(self): component1 = self.getComponent() component2 = self.getComponent() with self.assertWarnsRegex(DeprecationWarning, "Component.skew()"): component1.skew((100, 200), offset=(1, 2)) self.assertNotEqual(component1.bounds, component2.bounds) component2.skewBy((100, 200), origin=(1, 2)) self.assertEqual(component1.bounds, component2.bounds) # ----- # Point # ----- def getPoint(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") point = contour.points[1] point.smooth = True return point def test_point_removed_select(self): point = self.getPoint() with self.assertRaisesRegex(RemovedError, "Point.select"): point.select() def test_point_removed_setParent(self): point = self.getPoint() with self.assertRaisesRegex(RemovedError, "setParent()"): point.setParent(None) def test_point_deprecated__generateIdentifier(self): point = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point._getIdentifier()"): i = point._generateIdentifier() self.assertEqual(i, point._getIdentifier()) def test_point_deprecated_generateIdentifier(self): point = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.getIdentifier()"): i = point.generateIdentifier() self.assertEqual(i, point.getIdentifier()) def test_point_getParent(self): point = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.contour"): p = point.getParent() self.assertEqual(p, point.contour) def test_point_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. point = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.changed()"): point.update() def test_point_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. point = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.changed()"): point.setChanged() def test_point_deprecated_move(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.move()"): point1.move((0, 20)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.moveBy((0, 20)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_translate(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.translate()"): point1.translate((0, 20)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.moveBy((0, 20)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_scale_no_center(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.scale()"): point1.scale((-2)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.scaleBy((-2)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_scale_center(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.scale()"): point1.scale((-2, 3), center=(1, 2)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_rotate_no_offset(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.rotate()"): point1.rotate(45) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.rotateBy(45) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_rotate_offset(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.rotate()"): point1.rotate(45, offset=(1, 2)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.rotateBy(45, origin=(1, 2)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_transform(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.transform()"): point1.transform((2, 0, 0, 3, -3, 2)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_skew_no_offset_one_value(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.skew()"): point1.skew(100) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.skewBy(100) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_skew_no_offset_two_values(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.skew()"): point1.skew((100, 200)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.skewBy((100, 200)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_skew_offset_one_value(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.skew()"): point1.skew(100, offset=(1, 2)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.skewBy(100, origin=(1, 2)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) def test_point_deprecated_skew_offset_two_values(self): point1 = self.getPoint() point2 = self.getPoint() with self.assertWarnsRegex(DeprecationWarning, "Point.skew()"): point1.skew((100, 200), offset=(1, 2)) self.assertNotEqual((point1.x, point1.y), (point2.x, point2.y)) point2.skewBy((100, 200), origin=(1, 2)) self.assertEqual((point1.x, point1.y), (point2.x, point2.y)) # ------ # bPoint # ------ def getBPoint(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") contour.appendPoint((303, 0), "line") bPoint = contour.bPoints[1] return bPoint def test_bPoint_removed_select(self): bPoint = self.getBPoint() with self.assertRaisesRegex(RemovedError, "BPoint.select"): bPoint.select() def test_bPoint_removed_setParent(self): bPoint = self.getBPoint() with self.assertRaisesRegex(RemovedError, "setParent()"): bPoint.setParent(None) def test_bPoint_deprecated__generateIdentifier(self): bPoint = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint._getIdentifier()"): i = bPoint._generateIdentifier() self.assertEqual(i, bPoint._getIdentifier()) def test_bPoint_deprecated_generateIdentifier(self): bPoint = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.getIdentifier()"): i = bPoint.generateIdentifier() self.assertEqual(i, bPoint.getIdentifier()) def test_bPoint_getParent(self): bPoint = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.contour"): p = bPoint.getParent() self.assertEqual(p, bPoint.contour) def test_bPoint_deprecated_update(self): # As changed() is defined by the environment, only test if a Warning is issued. bPoint = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.changed()"): bPoint.update() def test_bPoint_deprecated_setChanged(self): # As changed() is defined by the environment, only test if a Warning is issued. bPoint = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.changed()"): bPoint.setChanged() def test_bPoint_deprecated_move(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.move()"): bPoint1.move((0, 20)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.moveBy((0, 20)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_translate(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.translate()"): bPoint1.translate((0, 20)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.moveBy((0, 20)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_scale_no_center(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.scale()"): bPoint1.scale((-2)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.scaleBy((-2)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_scale_center(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.scale()"): bPoint1.scale((-2, 3), center=(1, 2)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_rotate_no_offset(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.rotate()"): bPoint1.rotate(45) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.rotateBy(45) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_rotate_offset(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.rotate()"): bPoint1.rotate(45, offset=(1, 2)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.rotateBy(45, origin=(1, 2)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_transform(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.transform()"): bPoint1.transform((2, 0, 0, 3, -3, 2)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_skew_no_offset_one_value(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.skew()"): bPoint1.skew(100) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.skewBy(100) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_skew_no_offset_two_values(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.skew()"): bPoint1.skew((100, 200)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.skewBy((100, 200)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_skew_offset_one_value(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.skew()"): bPoint1.skew(100, offset=(1, 2)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.skewBy(100, origin=(1, 2)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) def test_bPoint_deprecated_skew_offset_two_values(self): bPoint1 = self.getBPoint() bPoint2 = self.getBPoint() with self.assertWarnsRegex(DeprecationWarning, "BPoint.skew()"): bPoint1.skew((100, 200), offset=(1, 2)) self.assertNotEqual(bPoint1.anchor, bPoint2.anchor) bPoint2.skewBy((100, 200), origin=(1, 2)) self.assertEqual(bPoint1.anchor, bPoint2.anchor) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_features.py000066400000000000000000000074361477533125200250540ustar00rootroot00000000000000import unittest import collections class TestFeatures(unittest.TestCase): def getFeatures_generic(self): features, _ = self.objectGenerator("features") features.text = "# test" return features # ---- # repr # ---- def test_reprContents(self): font, _ = self.objectGenerator("font") features = font.features features.text = "# test" value = features._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_noFont(self): features, _ = self.objectGenerator("features") features.text = "# test" value = features._reprContents() self.assertIsInstance(value, list) self.assertListEqual(value, []) # ---- # Text # ---- def test_text_get(self): features = self.getFeatures_generic() self.assertEqual( features.text, "# test" ) def test_text_valid_set(self): features = self.getFeatures_generic() features.text = "# foo" self.assertEqual( features.text, "# foo" ) def test_text_set_none(self): features = self.getFeatures_generic() features.text = None self.assertIsNone(features.text) def test_text_invalid_set(self): features = self.getFeatures_generic() with self.assertRaises(TypeError): features.text = 123 # ------- # Parents # ------- def test_get_parent_font(self): font, _ = self.objectGenerator("font") features = font.features features.text = "# Test" self.assertIsNotNone(features.font) self.assertEqual( features.font, font ) def test_get_parent_noFont(self): features = self.getFeatures_generic() self.assertIsNone(features.font) def test_set_parent_font(self): font, _ = self.objectGenerator("font") features = self.getFeatures_generic() features.font = font self.assertIsNotNone(features.font) self.assertEqual( features.font, font ) def test_set_parent_font_none(self): features = self.getFeatures_generic() features.font = None self.assertIsNone(features.font) def test_set_parent_font_exists(self): font, _ = self.objectGenerator("font") otherFont, _ = self.objectGenerator("font") features = font.features features.text = "# Test" with self.assertRaises(AssertionError): features.font = otherFont # ---- # Hash # ---- def test_hash(self): features = self.getFeatures_generic() self.assertEqual( isinstance(features, collections.abc.Hashable), True ) # -------- # Equality # -------- def test_object_equal_self(self): features_one = self.getFeatures_generic() self.assertEqual( features_one, features_one ) def test_object_not_equal_other(self): features_one = self.getFeatures_generic() features_two = self.getFeatures_generic() self.assertNotEqual( features_one, features_two ) def test_object_equal_self_variable_assignment(self): features_one = self.getFeatures_generic() a = features_one a.text += "# testing" self.assertEqual( features_one, a ) def test_object_not_equal_self_variable_assignment(self): features_one = self.getFeatures_generic() features_two = self.getFeatures_generic() a = features_one self.assertNotEqual( features_two, a ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_font.py000066400000000000000000000410601477533125200241730ustar00rootroot00000000000000import unittest import collections import tempfile import os import shutil class TestFont(unittest.TestCase): # ------ # Layers # ------ def getFont_layers(self): font, _ = self.objectGenerator("font") for name in "ABCD": font.newLayer("layer " + name) return font def test_getLayer_unknown(self): font = self.getFont_layers() with self.assertRaises(ValueError): font.getLayer("There is no layer with this name.") # ------ # Glyphs # ------ def getFont_glyphs(self): font, _ = self.objectGenerator("font") for name in "ABCD": font.newGlyph(name) return font def getFont_guidelines(self): font, _ = self.objectGenerator("font") font.appendGuideline((1, 2), 0, "Test Guideline 1") font.appendGuideline((3, 4), 90, "Test Guideline 2") return font def test_appendGuideline_valid_object(self): font, _ = self.objectGenerator("font") src, _ = self.objectGenerator("guideline") src.position = (1, 2) src.angle = 123 src.name = "test" src.color = (1, 1, 1, 1) src.getIdentifier() dst = font.appendGuideline(guideline=src) self.assertNotEqual(src, dst) self.assertEqual(src.position, dst.position) self.assertEqual(src.angle, dst.angle) self.assertEqual(src.name, dst.name) self.assertEqual(src.color, dst.color) self.assertEqual(src.identifier, dst.identifier) # glyphOrder def test_glyphOrder(self): font = self.getFont_glyphs() expectedGlyphOrder = ["A", "B", "C", "D"] self.assertEqual(font.glyphOrder, tuple(expectedGlyphOrder)) # reverse the excepected glyph order and set it expectedGlyphOrder.reverse() font.glyphOrder = expectedGlyphOrder self.assertEqual(font.glyphOrder, tuple(expectedGlyphOrder)) # add a glyph expectedGlyphOrder.append("newGlyph") font.newGlyph("newGlyph") self.assertEqual(font.glyphOrder, tuple(expectedGlyphOrder)) # remove a glyph expectedGlyphOrder.remove("newGlyph") del font["newGlyph"] self.assertEqual(font.glyphOrder, tuple(expectedGlyphOrder)) # insert a glyph, where the glyph is at the beginning of the glyph order glyph, _ = self.objectGenerator("glyph") font["D"] = glyph self.assertEqual(font.glyphOrder, tuple(expectedGlyphOrder)) # insert a glyph, where the glyph is at the end of the glyph order glyph, _ = self.objectGenerator("glyph") font["A"] = glyph self.assertEqual(font.glyphOrder, tuple(expectedGlyphOrder)) # len def test_len_initial(self): font = self.getFont_glyphs() self.assertEqual( len(font), 4 ) def test_len_two_layers(self): font = self.getFont_glyphs() layer = font.newLayer("test") layer.newGlyph("X") self.assertEqual( len(font), 4 ) # insert glyphs def test_insertGlyph(self): font, _ = self.objectGenerator("font") glyph, _ = self.objectGenerator("glyph") font.insertGlyph(glyph, "inserted1") self.assertIn("inserted1", font) font["inserted2"] = glyph self.assertIn("inserted2", font) font.newGlyph("A") glyph.unicode = 123 font["A"] = glyph self.assertEqual(font["A"].unicode, 123) # ---- # flatKerning # ---- def test_flatKerning(self): font = self.getFont_glyphs() # glyph, glyph kerning font.kerning["A", "V"] = -100 font.kerning["V", "A"] = -200 expected = {("A", "V"): -100, ("V", "A"): -200} self.assertEqual(font.getFlatKerning(), expected) # add some groups font.groups["public.kern1.O"] = ["O", "Ograve"] font.groups["public.kern2.O"] = ["O", "Ograve"] # group, group kerning font.kerning["public.kern1.O", "public.kern2.O"] = -50 expected = { ('O', 'O'): -50, ('Ograve', 'O'): -50, ('O', 'Ograve'): -50, ('Ograve', 'Ograve'): -50, ('A', 'V'): -100, ('V', 'A'): -200 } self.assertEqual(font.getFlatKerning(), expected) # glyph, group exception font.kerning["O", "public.kern2.O"] = -30 expected = { ('O', 'O'): -30, ('Ograve', 'O'): -50, ('O', 'Ograve'): -30, ('Ograve', 'Ograve'): -50, ('A', 'V'): -100, ('V', 'A'): -200 } self.assertEqual(font.getFlatKerning(), expected) # glyph, glyph exception font.kerning["O", "Ograve"] = -70 expected = { ('O', 'O'): -30, ('Ograve', 'O'): -50, ('O', 'Ograve'): -70, ('Ograve', 'Ograve'): -50, ('A', 'V'): -100, ('V', 'A'): -200 } self.assertEqual(font.getFlatKerning(), expected) # ---- # Hash # ---- def test_hash_same_object(self): font_one = self.getFont_glyphs() self.assertEqual( hash(font_one), hash(font_one) ) def test_hash_different_object(self): font_one = self.getFont_glyphs() font_two = self.getFont_glyphs() self.assertNotEqual( hash(font_one), hash(font_two) ) def test_hash_same_object_variable_assignment(self): font_one = self.getFont_glyphs() a = font_one self.assertEqual( hash(font_one), hash(a) ) def test_hash_different_object_variable_assignment(self): font_one = self.getFont_glyphs() font_two = self.getFont_glyphs() a = font_one self.assertNotEqual( hash(font_two), hash(a) ) def test_hash_is_hasbable(self): font_one = self.getFont_glyphs() self.assertEqual( isinstance(font_one, collections.abc.Hashable), True ) # -------- # Equality # -------- def test_object_equal_self(self): font_one = self.getFont_glyphs() self.assertEqual( font_one, font_one ) def test_object_not_equal_other(self): font_one = self.getFont_glyphs() font_two = self.getFont_glyphs() self.assertNotEqual( font_one, font_two ) def test_object_equal_self_variable_assignment(self): font_one = self.getFont_glyphs() a = font_one a.newGlyph("XYZ") self.assertEqual( font_one, a ) def test_object_not_equal_other_variable_assignment(self): font_one = self.getFont_glyphs() font_two = self.getFont_glyphs() a = font_one self.assertNotEqual( font_two, a ) # --------- # Selection # --------- # Font def test_selected_true(self): font = self.getFont_glyphs() try: font.selected = False except NotImplementedError: return font.selected = True self.assertEqual( font.selected, True ) def test_selected_false(self): font = self.getFont_glyphs() try: font.selected = False except NotImplementedError: return self.assertEqual( font.selected, False ) # Layers def test_selectedLayer_default(self): font = self.getFont_layers() try: font.defaultLayer.selected = False except NotImplementedError: return self.assertEqual( font.selectedLayers, () ) def test_selectedLayer_setSubObject(self): font = self.getFont_layers() try: font.defaultLayer.selected = False except NotImplementedError: return layer1 = font.getLayer("layer A") layer2 = font.getLayer("layer B") layer1.selected = True layer2.selected = True self.assertEqual( font.selectedLayers, (layer1, layer2) ) def test_selectedLayer_setFilledList(self): font = self.getFont_layers() try: font.defaultLayer.selected = False except NotImplementedError: return layer3 = font.getLayer("layer C") layer4 = font.getLayer("layer D") font.selectedLayers = [layer3, layer4] self.assertEqual( font.selectedLayers, (layer3, layer4) ) def test_selectedLayer_setEmptyList(self): font = self.getFont_layers() try: font.defaultLayer.selected = False except NotImplementedError: return layer1 = font.getLayer("layer A") layer1.selected = True font.selectedLayers = [] self.assertEqual( font.selectedLayers, () ) # Glyphs def test_selectedGlyphs_default(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return self.assertEqual( font.selectedGlyphs, () ) def test_selectedGlyphs_setSubObject(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return glyph1 = font["A"] glyph2 = font["B"] glyph1.selected = True glyph2.selected = True self.assertEqual( tuple(sorted(font.selectedGlyphs, key=lambda glyph: glyph.name)), (glyph1, glyph2) ) def test_selectedGlyphs_setFilledList(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return glyph3 = font["C"] glyph4 = font["D"] font.selectedGlyphs = [glyph3, glyph4] self.assertEqual( tuple(sorted(font.selectedGlyphs, key=lambda glyph: glyph.name)), (glyph3, glyph4) ) def test_selectedGlyphs_setEmptyList(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return glyph1 = font["A"] glyph1.selected = True font.selectedGlyphs = [] self.assertEqual( font.selectedGlyphs, () ) # Glyph names def test_selectedGlyphNames_default(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return self.assertEqual( font.selectedGlyphs, () ) def test_selectedGlyphNames_setSubObject(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return glyph1 = font["A"] glyph2 = font["B"] glyph1.selected = True glyph2.selected = True self.assertEqual( tuple(sorted(font.selectedGlyphNames)), ("A", "B") ) def test_selectedGlyphNames_setFilledList(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return font.selectedGlyphNames = ["C", "D"] self.assertEqual( tuple(sorted(font.selectedGlyphNames)), ("C", "D") ) def test_selectedGlyphNames_setEmptyList(self): font = self.getFont_glyphs() try: font.defaultLayer.selected = False except NotImplementedError: return glyph1 = font["A"] glyph1.selected = True font.selectedGlyphNames = [] self.assertEqual( font.selectedGlyphNames, () ) # Guidelines def test_selectedGuidelines_default(self): font = self.getFont_guidelines() guideline1 = font.guidelines[0] try: guideline1.selected = False except NotImplementedError: return self.assertEqual( font.selectedGuidelines, () ) def test_selectedGuidelines_setSubObject(self): font = self.getFont_guidelines() guideline1 = font.guidelines[0] guideline2 = font.guidelines[1] try: guideline1.selected = False except NotImplementedError: return guideline2.selected = True self.assertEqual( font.selectedGuidelines, (guideline2,) ) def test_selectedGuidelines_setFilledList(self): font = self.getFont_guidelines() guideline1 = font.guidelines[0] guideline2 = font.guidelines[1] try: guideline1.selected = False except NotImplementedError: return font.selectedGuidelines = [guideline1, guideline2] self.assertEqual( font.selectedGuidelines, (guideline1, guideline2) ) def test_selectedGuidelines_setEmptyList(self): font = self.getFont_guidelines() guideline1 = font.guidelines[0] try: guideline1.selected = True except NotImplementedError: return font.selectedGuidelines = [] self.assertEqual( font.selectedGuidelines, () ) # save def _saveFontPath(self, ext): root = tempfile.mkdtemp() return os.path.join(root, "test.%s" % ext) def _tearDownPath(self, path): if os.path.isdir(path): shutil.rmtree(path) elif os.path.isfile(path): os.remove(path) def _save(self, testCallback, **kwargs): path = self._saveFontPath(".ufo") font = self.getFont_glyphs() font.save(path, **kwargs) font.close() testCallback(path) self._tearDownPath(path) def test_save(self): def testCases(path): self.assertTrue(os.path.exists(path) and os.path.isdir(path)) self._save(testCases) def test_save_formatVersion(self): from fontTools.ufoLib import UFOReader for version in [2, 3]: # fails on formatVersion 1 (but maybe we should not worry about it...) def testCases(path): reader = UFOReader(path) self.assertEqual(reader.formatVersion, version) self._save(testCases, formatVersion=version) def test_save_fileStructure(self): from fontTools.ufoLib import UFOReader, UFOFileStructure for fileStructure in [None, "package", "zip"]: def testCases(path): reader = UFOReader(path) expectedFileStructure = fileStructure if fileStructure is None: expectedFileStructure = UFOFileStructure.PACKAGE else: expectedFileStructure = UFOFileStructure(fileStructure) self.assertEqual(reader.fileStructure, expectedFileStructure) self._save(testCases, fileStructure=fileStructure) # copy def test_copy(self): font = self.getFont_glyphs() copy = font.copy() self.assertEqual( font.keys(), copy.keys() ) font = self.getFont_glyphs() font.defaultLayer.name = "hello" copy = font.copy() self.assertEqual( font.keys(), copy.keys() ) self.assertEqual( font.defaultLayerName, copy.defaultLayerName ) font = self.getFont_guidelines() copy = font.copy() self.assertEqual( copy.selectedGuidelines, font.selectedGuidelines ) # ------------- # Interpolation # ------------- def test_interpolate_global_guidelines(self): interpolated_font, _ = self.objectGenerator("font") font_min, _ = self.objectGenerator("font") font_min.appendGuideline(position=(0, 0), angle=0) font_max, _ = self.objectGenerator("font") font_max.appendGuideline(position=(200, 200), angle=0) interpolated_font.info.interpolate(0.5, font_min.info, font_max.info, round=True) self.assertEqual( len(interpolated_font.guidelines), 1 ) self.assertEqual( interpolated_font.guidelines[0].position, (100, 100) )robotools-fontParts-26e8b8c/Lib/fontParts/test/test_fuzzyNumber.py000066400000000000000000000045111477533125200255650ustar00rootroot00000000000000import unittest from fontParts.base.base import FuzzyNumber class TestFuzzyNumber(unittest.TestCase): def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) def test_init(self): fuzzyNumber1 = FuzzyNumber(value=0, threshold=1) fuzzyNumber2 = FuzzyNumber(2, 3) self.assertEqual([fuzzyNumber1.value, fuzzyNumber1.threshold], [0, 1]) self.assertEqual([fuzzyNumber2.value, fuzzyNumber2.threshold], [2, 3]) def test_repr(self): fuzzyNumber = FuzzyNumber(0, 1) self.assertEqual(repr(fuzzyNumber), "[0.000000 1.000000]") def test_comparison(self): fuzzyNumber1 = FuzzyNumber(value=0, threshold=1) self.assertEqual(fuzzyNumber1, 0) self.assertTrue(fuzzyNumber1 < 1) self.assertFalse(fuzzyNumber1 < -0.000001) self.assertFalse(fuzzyNumber1 < 0) fuzzyNumber2 = FuzzyNumber(value=0.999999, threshold=1) self.assertEqual( repr(sorted([fuzzyNumber1, fuzzyNumber2])), "[[0.000000 1.000000], [0.999999 1.000000]]" ) self.assertFalse(fuzzyNumber1 < fuzzyNumber2) fuzzyNumber2 = FuzzyNumber(value=1, threshold=1) self.assertEqual( repr(sorted([fuzzyNumber1, fuzzyNumber2])), "[[0.000000 1.000000], [1.000000 1.000000]]" ) self.assertTrue(fuzzyNumber1 < fuzzyNumber2) fuzzyNumber2 = FuzzyNumber(value=-0.999999, threshold=1) self.assertEqual( repr(sorted([fuzzyNumber1, fuzzyNumber2])), "[[0.000000 1.000000], [-0.999999 1.000000]]" ) self.assertFalse(fuzzyNumber1 > fuzzyNumber2) fuzzyNumber2 = FuzzyNumber(value=-1, threshold=1) self.assertEqual( repr(sorted([fuzzyNumber1, fuzzyNumber2])), "[[-1.000000 1.000000], [0.000000 1.000000]]" ) self.assertTrue(fuzzyNumber1 > fuzzyNumber2) # equal self.assertEqual(fuzzyNumber1, fuzzyNumber1) self.assertNotEqual(fuzzyNumber1, fuzzyNumber2) # complex sorting fuzzyNumber2 = FuzzyNumber(value=0.999999, threshold=1) self.assertEqual( repr(sorted([(fuzzyNumber1, 20), (fuzzyNumber2, 10)])), "[([0.999999 1.000000], 10), ([0.000000 1.000000], 20)]" ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_glyph.py000066400000000000000000001305551477533125200243600ustar00rootroot00000000000000import unittest import collections from fontParts.base import FontPartsError from .test_image import testImageData class TestGlyph(unittest.TestCase): def getGlyph_generic(self): glyph, _ = self.objectGenerator("glyph") glyph.name = "Test Glyph 1" glyph.unicode = int(ord("X")) glyph.width = 250 glyph.height = 750 pen = glyph.getPen() pen.moveTo((100, -10)) pen.lineTo((100, 100)) pen.lineTo((200, 100)) pen.lineTo((200, 0)) pen.closePath() pen.moveTo((110, 10)) pen.lineTo((110, 90)) pen.lineTo((190, 90)) pen.lineTo((190, 10)) pen.closePath() glyph.appendAnchor("Test Anchor 1", (1, 2)) glyph.appendAnchor("Test Anchor 2", (3, 4)) glyph.appendGuideline((1, 2), 0, "Test Guideline 1") glyph.appendGuideline((3, 4), 90, "Test Guideline 2") return glyph def getGlyph_empty(self): glyph, _ = self.objectGenerator("glyph") glyph.name = "Test Glyph 2" glyph.unicode = int(ord("X")) glyph.width = 0 glyph.height = 0 return glyph def get_generic_object(self, obj_name): fp_object, _ = self.objectGenerator(obj_name) return fp_object # ------- # Parents # ------- def test_get_layer(self): font = self.get_generic_object("font") layer = font.layers[0] glyph = layer.newGlyph("A") self.assertEqual( glyph.layer, layer ) def test_get_layer_orphan_glyph(self): glyph = self.get_generic_object("glyph") self.assertIsNone(glyph.layer) def test_get_font(self): font = self.get_generic_object("font") glyph = font.newGlyph("A") self.assertEqual( glyph.font, font ) def test_get_font_orphan_glyph(self): glyph = self.get_generic_object("glyph") self.assertIsNone(glyph.font) # -------------- # Identification # -------------- def test_get_name(self): glyph = self.getGlyph_generic() self.assertEqual( glyph.name, "Test Glyph 1" ) def test_get_name_not_set(self): glyph = self.get_generic_object("glyph") self.assertIsNone( glyph.name ) def test_set_name_valid(self): glyph = self.getGlyph_generic() name = "Test Glyph 1" # the name is intentionally the same glyph.name = name self.assertEqual( glyph.name, name ) def test_set_name_invalid(self): invalid_names = ( ("", ValueError), ("A", ValueError), # a glyph with this name already exists (3, TypeError), (None, TypeError) ) font = self.get_generic_object("font") font.newGlyph("A") glyph = font.newGlyph("B") for name, err in invalid_names: with self.assertRaises(err): glyph.name = name def test_get_unicode(self): glyph = self.getGlyph_generic() self.assertEqual( glyph.unicode, 88 ) def test_get_unicode_not_set(self): glyph = self.get_generic_object("glyph") self.assertIsNone( glyph.unicode ) def test_set_unicode_valid(self): valid_uni_values = (100, None, 0x6D, '6D') for value in valid_uni_values: glyph = self.get_generic_object("glyph") glyph.unicode = value result = int(value, 16) if isinstance(value, str) else value self.assertEqual( glyph.unicode, result ) def test_set_unicode_value(self): glyph = self.get_generic_object("glyph") glyph.unicodes = (10, 20) glyph.unicode = 20 self.assertEqual( glyph.unicodes, (20,) ) def test_set_unicode_value_none(self): glyph = self.get_generic_object("glyph") glyph.unicodes = (10, 20) glyph.unicode = None self.assertEqual( glyph.unicodes, () ) def test_set_unicode_invalid(self): invalid_uni_values = ( ('GG', ValueError), (True, TypeError), ([], TypeError) ) glyph = self.get_generic_object("glyph") for value, err in invalid_uni_values: with self.assertRaises(err): glyph.unicode = value def test_get_unicodes(self): glyph = self.getGlyph_generic() self.assertEqual( glyph.unicodes, (88,) ) def test_get_unicodes_not_set(self): glyph = self.get_generic_object("glyph") self.assertEqual( glyph.unicodes, () ) def test_set_unicodes_valid(self): valid_uni_values = ([100, 200], [], (300,), ()) for values in valid_uni_values: glyph = self.get_generic_object("glyph") glyph.unicodes = values self.assertEqual( glyph.unicodes, tuple(values) ) def test_set_unicodes_invalid(self): invalid_uni_values = ( ('GG', ValueError), (True, TypeError), (30, TypeError) ) glyph = self.get_generic_object("glyph") for value, err in invalid_uni_values: with self.assertRaises(err): glyph.unicodes = value def test_set_unicodes_duplicates(self): glyph = self.get_generic_object("glyph") with self.assertRaises(ValueError): glyph.unicodes = (200, 110, 110) # ------- # Metrics # ------- # The methods are dynamically generated by test_generator() # Methods to test that empty glyphs have margins of None def test_get_leftMargin_not_set(self): glyph = self.get_generic_object("glyph") self.assertIsNone( glyph.leftMargin ) def test_get_rightMargin_not_set(self): glyph = self.get_generic_object("glyph") self.assertIsNone( glyph.rightMargin ) def test_get_bottomMargin_not_set(self): glyph = self.get_generic_object("glyph") self.assertIsNone( glyph.bottomMargin ) def test_get_topMargin_not_set(self): glyph = self.get_generic_object("glyph") self.assertIsNone( glyph.topMargin ) # ------- # Queries # ------- def test_get_bounds(self): glyph = self.getGlyph_generic() self.assertEqual( glyph.bounds, (100, -10, 200, 100) ) # ------ # Layers # ------ def test_get_layers(self): font = self.get_generic_object("font") glyph = font.newGlyph("A") layers = glyph.layers self.assertEqual(len(layers), 1) self.assertEqual( glyph.layer.name, font.defaultLayerName ) self.assertEqual( layers[0], glyph # a glyph layer is really just a glyph ) self.assertEqual( layers[0].name, 'A' ) def test_get_layers_orphan_glyph(self): glyph = self.getGlyph_generic() self.assertEqual( glyph.layers, () ) def test_getLayer_valid(self): font = self.get_generic_object("font") glyph = font.newGlyph("B") self.assertEqual( glyph.getLayer(font.defaultLayerName).name, 'B' ) def test_getLayer_valid_not_found(self): font = self.get_generic_object("font") glyph = font.newGlyph("B") with self.assertRaises(ValueError): # No layer named 'layer_name' in glyph 'B' glyph.getLayer('layer_name') def test_getLayer_invalid(self): font = self.get_generic_object("font") glyph = font.newGlyph("B") with self.assertRaises(TypeError): glyph.getLayer() with self.assertRaises(TypeError): glyph.getLayer(None) with self.assertRaises(TypeError): glyph.getLayer(0) with self.assertRaises(ValueError): # Layer names must be at least one character long glyph.getLayer('') def test_newLayer_valid(self): font = self.get_generic_object("font") glyph = font.newGlyph("C") self.assertEqual(len(glyph.layers), 1) layer = glyph.newLayer("background") self.assertEqual(len(glyph.layers), 2) self.assertEqual(layer.name, 'C') def test_newLayer_valid_already_exists(self): font = self.get_generic_object("font") glyph = font.newGlyph("C") self.assertEqual(len(glyph.layers), 1) glyph.newLayer("mask") self.assertEqual(len(glyph.layers), 2) glyph.newLayer("mask") # intentional duplicate line self.assertEqual(len(glyph.layers), 2) def test_newLayer_invalid(self): font = self.get_generic_object("font") glyph = font.newGlyph("C") with self.assertRaises(TypeError): glyph.newLayer() with self.assertRaises(TypeError): glyph.newLayer(0) with self.assertRaises(ValueError): # Layer names must be at least one character long glyph.newLayer('') def test_removeLayer_valid_type_string(self): font = self.get_generic_object("font") glyph = font.newGlyph("D") self.assertEqual(len(glyph.layers), 1) glyph.removeLayer(font.defaultLayerName) self.assertEqual(len(glyph.layers), 0) def test_removeLayer_valid_type_glyph_layer(self): font = self.get_generic_object("font") glyph = font.newGlyph("D") self.assertEqual(len(glyph.layers), 1) glyph.removeLayer(glyph.layers[0]) self.assertEqual(len(glyph.layers), 0) def test_removeLayer_valid_not_found_type_string(self): font = self.get_generic_object("font") glyph = font.newGlyph("D") with self.assertRaises(ValueError): # No layer named 'layer_name' in glyph 'D' glyph.removeLayer('layer_name') def test_removeLayer_invalid(self): font = self.get_generic_object("font") glyph = font.newGlyph("D") with self.assertRaises(TypeError): glyph.removeLayer() with self.assertRaises(TypeError): glyph.removeLayer(0) with self.assertRaises(ValueError): # Layer names must be at least one character long glyph.removeLayer('') # ------ # Global # ------ def test_clear(self): glyph = self.getGlyph_generic() glyph.appendComponent("component 1") self.assertEqual(len(glyph), 2) self.assertEqual(len(glyph.components), 1) self.assertEqual(len(glyph.anchors), 2) self.assertEqual(len(glyph.guidelines), 2) glyph.clear(contours=False, components=False, anchors=False, guidelines=False, image=False) glyph.clear() self.assertEqual(len(glyph), 0) self.assertEqual(len(glyph.components), 0) self.assertEqual(len(glyph.anchors), 0) self.assertEqual(len(glyph.guidelines), 0) def test_appendGlyph(self): glyph_one = self.getGlyph_generic() glyph_two = self.getGlyph_generic() glyph_one.appendComponent("component 1") glyph_two.appendComponent("component 2") self.assertEqual(len(glyph_one), 2) self.assertEqual(len(glyph_one.components), 1) self.assertEqual(len(glyph_one.anchors), 2) self.assertEqual(len(glyph_one.guidelines), 2) glyph_one.appendGlyph(glyph_two) glyph_one.appendGlyph(glyph_two, (300, -40)) self.assertEqual(len(glyph_one), 6) self.assertEqual(len(glyph_one.components), 3) self.assertEqual(len(glyph_one.anchors), 6) self.assertEqual(len(glyph_one.guidelines), 6) # -------- # Contours # -------- def test_get_contour_invalid(self): glyph = self.getGlyph_generic() with self.assertRaises(ValueError): # No contour located at index 5 glyph[5] def test_appendContour_offset_valid(self): glyph = self.getGlyph_generic() contour = self.get_generic_object("contour") contour.insertPoint(0, position=(0, 0)) contour.insertPoint(1, position=(100, 100)) contour.insertPoint(2, position=(0, 100)) self.assertEqual(len(glyph), 2) newcontour = glyph.appendContour(contour, (45, 50)) self.assertEqual(len(glyph), 3) self.assertEqual(newcontour, glyph[-1]) self.assertEqual(len(newcontour.points), 3) self.assertEqual(newcontour.points[0].x, 45) self.assertEqual(newcontour.points[0].y, 50) def test_removeContour_valid(self): glyph = self.getGlyph_generic() contour = self.get_generic_object("contour") glyph.appendContour(contour) contour1 = glyph.contours[1] self.assertEqual(len(glyph), 3) glyph.removeContour(contour1) self.assertEqual(len(glyph), 2) glyph.removeContour(0) self.assertEqual(len(glyph), 1) def test_removeContour_invalid(self): glyph = self.getGlyph_generic() with self.assertRaises(ValueError): # No contour located at index 5 glyph.removeContour(5) with self.assertRaises(FontPartsError): # The contour could not be found glyph.removeContour(self.get_generic_object("contour")) def test_clearContours(self): glyph = self.getGlyph_generic() self.assertEqual(len(glyph), 2) glyph.clearContours() self.assertEqual(len(glyph), 0) def test_autoContourOrder_points(self): glyph = self.getGlyph_empty() pen = glyph.getPen() pen.moveTo((287, 212)) pen.lineTo((217, 108)) pen.lineTo((407, 109)) pen.closePath() pen = glyph.getPen() pen.moveTo((73, 184)) pen.lineTo((39, 112)) pen.lineTo((147, 61)) pen.lineTo((140, 137)) pen.lineTo((110, 176)) pen.closePath() pen = glyph.getPen() pen.moveTo((60, 351)) pen.lineTo((149, 421)) pen.lineTo((225, 398)) pen.lineTo((237, 290)) pen.lineTo((183, 239)) pen.lineTo((129, 245)) pen.lineTo((70, 285)) pen.closePath() glyph.autoContourOrder() self.assertEqual([len(c.points) for c in glyph.contours], [7, 5, 3]) def test_autoContourOrder_segments(self): glyph = self.getGlyph_empty() pen = glyph.getPen() pen.moveTo((116, 202)) pen.curveTo((116, 308), (156, 348), (245, 348)) pen.closePath() pen = glyph.getPen() pen.moveTo((261, 212)) pen.lineTo((335, 212)) pen.lineTo((335, 301)) pen.lineTo((261, 301)) pen.closePath() glyph.autoContourOrder() self.assertEqual([len(c.segments) for c in glyph.contours], [4, 2]) def test_autoContourOrder_fuzzycenter(self): glyph = self.getGlyph_empty() # the contours are overlapping too much # the different position of their center points # should not matter pen = glyph.getPen() pen.moveTo((313, 44)) pen.curveTo((471, 44), (600, 174), (600, 332)) pen.curveTo((600, 490), (471, 619), (313, 619)) pen.curveTo((155, 619), (26, 490), (26, 332)) pen.curveTo((26, 174), (155, 44), (313, 44)) pen.closePath() pen = glyph.getPen() pen.moveTo((288, 122)) pen.curveTo((383, 122), (461, 200), (461, 295)) pen.curveTo((461, 390), (383, 468), (288, 468)) pen.curveTo((192, 468), (114, 390), (114, 295)) pen.curveTo((114, 200), (192, 122), (288, 122)) pen.closePath() glyph.autoContourOrder() self.assertTrue( glyph.contours[0].points[0].x == 313 and glyph.contours[1].points[0].x == 288 ) def test_autoContourOrder_distantCenter(self): glyph = self.getGlyph_empty() # both outlines have same structure # but their center points are far away # so, the center points should inform # the sorting # from left to right first, then bottom to top pen = glyph.getPen() pen.moveTo((313, 44)) pen.curveTo((471, 44), (600, 174), (600, 332)) pen.curveTo((600, 490), (471, 619), (313, 619)) pen.curveTo((155, 619), (26, 490), (26, 332)) pen.curveTo((26, 174), (155, 44), (313, 44)) pen.closePath() pen = glyph.getPen() pen.moveTo((-142, -118)) pen.curveTo((-47, -118), (31, -40), (31, 55)) pen.curveTo((31, 150), (-47, 228), (-142, 228)) pen.curveTo((-238, 228), (-316, 150), (-316, 55)) pen.curveTo((-316, -40), (-238, -118), (-142, -118)) pen.closePath() glyph.autoContourOrder() self.assertTrue( glyph.contours[0].points[0].x == -142 and glyph.contours[1].points[0].x == 313 ) def test_autoContourOrder_bboxsurface(self): glyph = self.getGlyph_empty() # both outlines share the same center # so, the surface of the bounding box should inform # the sorting, from larger to smaller pen = glyph.getPen() pen.moveTo((100, 50)) pen.curveTo((128, 50), (150, 72), (150, 100)) pen.curveTo((150, 128), (128, 150), (100, 150)) pen.curveTo((72, 150), (50, 128), (50, 100)) pen.curveTo((50, 72), (72, 50), (100, 50)) pen.closePath() pen = glyph.getPen() pen.moveTo((100, 0)) pen.curveTo((155, 0), (200, 45), (200, 100)) pen.curveTo((200, 155), (155, 200), (100, 200)) pen.curveTo((45, 200), (0, 155), (0, 100)) pen.curveTo((0, 45), (45, 0), (100, 0)) pen.closePath() glyph.autoContourOrder() self.assertTrue( glyph.contours[0].points[0].y == 0 and glyph.contours[1].points[0].y == 50 ) # ---------- # Components # ---------- # appendComponent def test_appendComponent_invalid_circularReference(self): glyph = self.getGlyph_generic() with self.assertRaises(FontPartsError): glyph.appendComponent(glyph.name) def test_appendComponent_valid_object(self): glyph = self.getGlyph_generic() src, _ = self.objectGenerator("component") src.baseGlyph = "test" src.transformation = (1, 2, 3, 4, 5, 6) src.getIdentifier() dst = glyph.appendComponent(component=src) self.assertNotEqual(src, dst) self.assertEqual(src.baseGlyph, dst.baseGlyph) self.assertEqual(src.transformation, dst.transformation) self.assertEqual(src.identifier, dst.identifier) def test_appendComponent_valid_object_baseGlyph(self): glyph = self.getGlyph_generic() src, _ = self.objectGenerator("component") src.baseGlyph = "assigned" dst = glyph.appendComponent(component=src, baseGlyph="argument") self.assertEqual(dst.baseGlyph, "argument") def test_appendComponent_valid_object_offset(self): glyph = self.getGlyph_generic() src, _ = self.objectGenerator("component") src.baseGlyph = "test" src.transformation = (1, 2, 3, 4, 5, 6) dst = glyph.appendComponent(component=src, offset=(-1, -2)) self.assertEqual(dst.offset, (-1, -2)) self.assertEqual(dst.transformation, (1, 2, 3, 4, -1, -2)) def test_appendComponent_valid_object_scale(self): glyph = self.getGlyph_generic() src, _ = self.objectGenerator("component") src.baseGlyph = "test" src.transformation = (1, 2, 3, 4, 5, 6) dst = glyph.appendComponent(component=src, scale=(-1, -2)) self.assertEqual(dst.scale, (-1, -2)) self.assertEqual(dst.transformation, (-1, 2, 3, -2, 5, 6)) def test_appendComponent_valid_object_conflictingIdentifier(self): glyph = self.getGlyph_generic() c = glyph.appendComponent("test") existingIdentifier = c.getIdentifier() src, _ = self.objectGenerator("component") src.baseGlyph = "test" src._setIdentifier(existingIdentifier) dst = glyph.appendComponent(component=src) self.assertNotEqual(src.identifier, dst.identifier) def test_appendComponent_invalid_object_circularReference(self): glyph = self.getGlyph_generic() src, _ = self.objectGenerator("component") src.baseGlyph = glyph.name with self.assertRaises(FontPartsError): glyph.appendComponent(glyph.name) # removeComponent def test_removeComponent_valid(self): glyph = self.getGlyph_generic() glyph.appendComponent("component 1") glyph.appendComponent("component 2") glyph.appendComponent("component 3") self.assertEqual(len(glyph.components), 3) component = glyph.components[1] glyph.removeComponent(component) self.assertEqual(len(glyph.components), 2) glyph.removeComponent(0) self.assertEqual(len(glyph.components), 1) def test_removeComponent_invalid(self): glyph = self.getGlyph_generic() with self.assertRaises(ValueError): # No component located at index 8 glyph.removeComponent(8) with self.assertRaises(FontPartsError): # The component could not be found glyph.removeComponent(self.get_generic_object("component")) # clearComponents def test_clearComponents(self): glyph = self.getGlyph_generic() glyph.appendComponent("component 1") self.assertEqual(len(glyph.components), 1) glyph.clearComponents() self.assertEqual(len(glyph.components), 0) # ------- # Anchors # ------- # appendAnchor def test_appendAnchor_valid_object(self): glyph = self.getGlyph_generic() src, _ = self.objectGenerator("anchor") src.name = "test" src.position = (1, 2) src.color = (1, 1, 1, 1) src.getIdentifier() dst = glyph.appendAnchor(anchor=src) self.assertNotEqual(src, dst) self.assertEqual(src.name, dst.name) self.assertEqual(src.position, dst.position) self.assertEqual(src.color, dst.color) self.assertEqual(src.identifier, dst.identifier) # removeAnchor def test_removeAnchor_valid(self): glyph = self.getGlyph_generic() glyph.appendAnchor("base", (250, 0), (1, 0, 0, 0.5)) anchor = glyph.anchors[1] self.assertEqual(len(glyph.anchors), 3) glyph.removeAnchor(anchor) self.assertEqual(len(glyph.anchors), 2) glyph.removeAnchor(0) self.assertEqual(len(glyph.anchors), 1) def test_removeAnchor_invalid(self): glyph = self.getGlyph_generic() with self.assertRaises(ValueError): # No anchor located at index 4 glyph.removeAnchor(4) with self.assertRaises(FontPartsError): # The anchor could not be found glyph.removeAnchor(self.get_generic_object("anchor")) # clearAnchors def test_clearAnchors(self): glyph = self.getGlyph_generic() self.assertEqual(len(glyph.anchors), 2) glyph.clearAnchors() self.assertEqual(len(glyph.anchors), 0) # ---------- # Guidelines # ---------- # appendGuideline def test_appendGuideline_valid_object(self): glyph = self.getGlyph_generic() src, _ = self.objectGenerator("guideline") src.position = (1, 2) src.angle = 123 src.name = "test" src.color = (1, 1, 1, 1) src.getIdentifier() dst = glyph.appendGuideline(guideline=src) self.assertNotEqual(src, dst) self.assertEqual(src.position, dst.position) self.assertEqual(src.angle, dst.angle) self.assertEqual(src.name, dst.name) self.assertEqual(src.color, dst.color) self.assertEqual(src.identifier, dst.identifier) # removeGuideline def test_removeGuideline_valid(self): glyph = self.getGlyph_generic() glyph.appendGuideline((5, -10), 90, None, (1, 0, 0, 0.5)) guideline = glyph.guidelines[1] self.assertEqual(len(glyph.guidelines), 3) glyph.removeGuideline(guideline) self.assertEqual(len(glyph.guidelines), 2) glyph.removeGuideline(0) self.assertEqual(len(glyph.guidelines), 1) def test_removeGuideline_invalid(self): glyph = self.getGlyph_generic() with self.assertRaises(ValueError): # No guideline located at index 6 glyph.removeGuideline(6) with self.assertRaises(FontPartsError): # The guideline could not be found glyph.removeGuideline(self.get_generic_object("guideline")) # clearGuidelines def test_clearGuidelines(self): glyph = self.getGlyph_generic() self.assertEqual(len(glyph.guidelines), 2) glyph.clearGuidelines() self.assertEqual(len(glyph.guidelines), 0) # ----- # Image # ----- def test_addImage(self): font = self.get_generic_object("font") glyph = font.newGlyph("glyphWithImage") image = glyph.addImage(data=testImageData) self.assertEqual( image.data, testImageData ) # ---- # Hash # ---- def test_hash_object_self(self): glyph_one = self.getGlyph_generic() glyph_one.name = "Test" self.assertEqual( hash(glyph_one), hash(glyph_one) ) def test_hash_object_other(self): glyph_one = self.getGlyph_generic() glyph_two = self.getGlyph_generic() glyph_one.name = "Test" glyph_two.name = "Test" self.assertNotEqual( hash(glyph_one), hash(glyph_two) ) def test_hash_object_self_variable_assignment(self): glyph_one = self.getGlyph_generic() a = glyph_one self.assertEqual( hash(glyph_one), hash(a) ) def test_hash_object_other_variable_assignment(self): glyph_one = self.getGlyph_generic() glyph_two = self.getGlyph_generic() a = glyph_one self.assertNotEqual( hash(glyph_two), hash(a) ) def test_is_hashable(self): glyph_one = self.getGlyph_generic() self.assertTrue( isinstance(glyph_one, collections.abc.Hashable) ) # -------- # Equality # -------- def test_object_equal_self(self): glyph_one = self.getGlyph_generic() glyph_one.name = "Test" self.assertEqual( glyph_one, glyph_one ) def test_object_not_equal_other(self): glyph_one = self.getGlyph_generic() glyph_two = self.getGlyph_generic() self.assertNotEqual( glyph_one, glyph_two ) def test_object_not_equal_other_name_same(self): glyph_one = self.getGlyph_generic() glyph_two = self.getGlyph_generic() glyph_one.name = "Test" glyph_two.name = "Test" self.assertNotEqual( glyph_one, glyph_two ) def test_object_equal_variable_assignment(self): glyph_one = self.getGlyph_generic() a = glyph_one a.name = "Other" self.assertEqual( glyph_one, a ) def test_object_not_equal_variable_assignment(self): glyph_one = self.getGlyph_generic() glyph_two = self.getGlyph_generic() a = glyph_one self.assertNotEqual( glyph_two, a ) # --------- # Selection # --------- def test_selected_true(self): glyph = self.getGlyph_generic() try: glyph.selected = False except NotImplementedError: return glyph.selected = True self.assertTrue( glyph.selected ) def test_not_selected_false(self): glyph = self.getGlyph_generic() try: glyph.selected = False except NotImplementedError: return self.assertFalse( glyph.selected ) # Contours def test_selectedContours_default(self): glyph = self.getGlyph_generic() contour1 = glyph.contours[0] try: contour1.selected = False except NotImplementedError: return self.assertEqual( glyph.selectedContours, () ) def test_selectedContours_setSubObject(self): glyph = self.getGlyph_generic() contour1 = glyph.contours[0] contour2 = glyph.contours[1] try: contour1.selected = False except NotImplementedError: return contour2.selected = True self.assertEqual( glyph.selectedContours, (contour2,) ) def test_selectedContours_setFilledList(self): glyph = self.getGlyph_generic() contour1 = glyph.contours[0] contour2 = glyph.contours[1] try: contour1.selected = False except NotImplementedError: return glyph.selectedContours = [contour1, contour2] self.assertEqual( glyph.selectedContours, (contour1, contour2) ) def test_selectedContours_setEmptyList(self): glyph = self.getGlyph_generic() contour1 = glyph.contours[0] try: contour1.selected = True except NotImplementedError: return glyph.selectedContours = [] self.assertEqual( glyph.selectedContours, () ) # Components def test_selectedComponents_default(self): glyph = self.getGlyph_generic() glyph.appendComponent("component 1") component1 = glyph.components[0] try: component1.selected = False except NotImplementedError: return self.assertEqual( glyph.selectedComponents, () ) def test_selectedComponents_setSubObject(self): glyph = self.getGlyph_generic() glyph.appendComponent("component 1") glyph.appendComponent("component 2") component1 = glyph.components[0] component2 = glyph.components[1] try: component1.selected = False except NotImplementedError: return component2.selected = True self.assertEqual( glyph.selectedComponents, (component2,) ) def test_selectedComponents_setFilledList(self): glyph = self.getGlyph_generic() glyph.appendComponent("component 1") glyph.appendComponent("component 2") component1 = glyph.components[0] component2 = glyph.components[1] try: component1.selected = False except NotImplementedError: return glyph.selectedComponents = [component1, component2] self.assertEqual( glyph.selectedComponents, (component1, component2) ) def test_selectedComponents_setEmptyList(self): glyph = self.getGlyph_generic() glyph.appendComponent("component 1") component1 = glyph.components[0] try: component1.selected = True except NotImplementedError: return glyph.selectedComponents = [] self.assertEqual( glyph.selectedComponents, () ) # Anchors def test_selectedAnchors_default(self): glyph = self.getGlyph_generic() anchor1 = glyph.anchors[0] try: anchor1.selected = False except NotImplementedError: return self.assertEqual( glyph.selectedAnchors, () ) def test_selectedAnchors_setSubObject(self): glyph = self.getGlyph_generic() anchor1 = glyph.anchors[0] anchor2 = glyph.anchors[1] try: anchor1.selected = False except NotImplementedError: return anchor2.selected = True self.assertEqual( glyph.selectedAnchors, (anchor2,) ) def test_selectedAnchors_setFilledList(self): glyph = self.getGlyph_generic() anchor1 = glyph.anchors[0] anchor2 = glyph.anchors[1] try: anchor1.selected = False except NotImplementedError: return glyph.selectedAnchors = [anchor1, anchor2] self.assertEqual( glyph.selectedAnchors, (anchor1, anchor2) ) def test_selectedAnchors_setEmptyList(self): glyph = self.getGlyph_generic() anchor1 = glyph.anchors[0] try: anchor1.selected = True except NotImplementedError: return glyph.selectedAnchors = [] self.assertEqual( glyph.selectedAnchors, () ) # Guidelines def test_selectedGuidelines_default(self): glyph = self.getGlyph_generic() guideline1 = glyph.guidelines[0] try: guideline1.selected = False except NotImplementedError: return self.assertEqual( glyph.selectedGuidelines, () ) def test_selectedGuidelines_setSubObject(self): glyph = self.getGlyph_generic() guideline1 = glyph.guidelines[0] guideline2 = glyph.guidelines[1] try: guideline1.selected = False except NotImplementedError: return guideline2.selected = True self.assertEqual( glyph.selectedGuidelines, (guideline2,) ) def test_selectedGuidelines_setFilledList(self): glyph = self.getGlyph_generic() guideline1 = glyph.guidelines[0] guideline2 = glyph.guidelines[1] try: guideline1.selected = False except NotImplementedError: return glyph.selectedGuidelines = [guideline1, guideline2] self.assertEqual( glyph.selectedGuidelines, (guideline1, guideline2) ) def test_selectedGuidelines_setEmptyList(self): glyph = self.getGlyph_generic() guideline1 = glyph.guidelines[0] try: guideline1.selected = True except NotImplementedError: return glyph.selectedGuidelines = [] self.assertEqual( glyph.selectedGuidelines, () ) # ------------- # Compatibility # ------------- def test_isCompatible_anchors(self): glyph1 = self.getGlyph_generic() glyph1.clearAnchors() glyph1.appendAnchor("a", (0, 0)) glyph1.appendAnchor("b", (0, 0)) glyph2 = self.getGlyph_generic() glyph2.clearAnchors() glyph2.appendAnchor("a", (0, 0)) glyph2.appendAnchor("b", (0, 0)) is_compatible, report = glyph1.isCompatible(glyph2) self.assertTrue(is_compatible) self.assertFalse(report.anchorDifferences) self.assertFalse(report.anchorOrderDifference) self.assertFalse(report.anchorCountDifference) self.assertFalse(report.anchorsMissingFromGlyph1) self.assertFalse(report.anchorsMissingFromGlyph2) def test_isCompatible_anchors_order(self): glyph1 = self.getGlyph_generic() glyph1.clearAnchors() glyph1.appendAnchor("a", (0, 0)) glyph1.appendAnchor("b", (0, 0)) glyph2 = self.getGlyph_generic() glyph2.clearAnchors() glyph2.appendAnchor("b", (0, 0)) glyph2.appendAnchor("a", (0, 0)) is_compatible, report = glyph1.isCompatible(glyph2) self.assertTrue(is_compatible) self.assertEqual(report.anchorDifferences, [(0, "a", "b"), (1, "b", "a")]) self.assertTrue(report.anchorOrderDifference) self.assertFalse(report.anchorCountDifference) self.assertFalse(report.anchorsMissingFromGlyph1) self.assertFalse(report.anchorsMissingFromGlyph2) def test_isCompatible_anchors_intersecting(self): glyph1 = self.getGlyph_generic() glyph1.clearAnchors() glyph1.appendAnchor("a", (0, 0)) glyph1.appendAnchor("b", (0, 0)) glyph2 = self.getGlyph_generic() glyph2.clearAnchors() glyph2.appendAnchor("a", (0, 0)) glyph2.appendAnchor("b", (0, 0)) glyph2.appendAnchor("b", (0, 0)) is_compatible, report = glyph1.isCompatible(glyph2) self.assertTrue(is_compatible) self.assertEqual(report.anchorDifferences, [(2, None, "b")]) self.assertFalse(report.anchorOrderDifference) self.assertTrue(report.anchorCountDifference) self.assertEqual(report.anchorsMissingFromGlyph1, ["b"]) self.assertFalse(report.anchorsMissingFromGlyph2) def test_isCompatible_anchors_disjoint(self): glyph1 = self.getGlyph_generic() glyph1.clearAnchors() glyph1.appendAnchor("x", (0, 0)) glyph2 = self.getGlyph_generic() glyph2.clearAnchors() glyph2.appendAnchor("a", (0, 0)) glyph2.appendAnchor("a", (0, 0)) glyph2.appendAnchor("b", (0, 0)) is_compatible, report = glyph1.isCompatible(glyph2) self.assertTrue(is_compatible) self.assertEqual( report.anchorDifferences, [(0, "x", "a"), (1, None, "a"), (2, None, "b")] ) self.assertFalse(report.anchorOrderDifference) self.assertTrue(report.anchorCountDifference) self.assertEqual(report.anchorsMissingFromGlyph1, ["a", "a", "b"]) self.assertEqual(report.anchorsMissingFromGlyph2, ["x"]) def test_isCompatible_components(self): glyph1, _ = self.objectGenerator("glyph") glyph1.appendComponent("a") glyph1.appendComponent("b") glyph2, _ = self.objectGenerator("glyph") glyph2.appendComponent("a") glyph2.appendComponent("b") is_compatible, report = glyph1.isCompatible(glyph2) self.assertTrue(is_compatible) self.assertFalse(report.componentDifferences) self.assertFalse(report.componentOrderDifference) self.assertFalse(report.componentCountDifference) self.assertFalse(report.componentsMissingFromGlyph1) self.assertFalse(report.componentsMissingFromGlyph2) def test_isCompatible_components_order(self): glyph1, _ = self.objectGenerator("glyph") glyph1.appendComponent("a") glyph1.appendComponent("b") glyph2, _ = self.objectGenerator("glyph") glyph2.appendComponent("b") glyph2.appendComponent("a") is_compatible, report = glyph1.isCompatible(glyph2) self.assertTrue(is_compatible) self.assertEqual(report.componentDifferences, [(0, "a", "b"), (1, "b", "a")]) self.assertTrue(report.componentOrderDifference) self.assertFalse(report.componentCountDifference) self.assertFalse(report.componentsMissingFromGlyph1) self.assertFalse(report.componentsMissingFromGlyph2) def test_isCompatible_components_intersecting(self): glyph1, _ = self.objectGenerator("glyph") glyph1.appendComponent("a") glyph1.appendComponent("b") glyph2, _ = self.objectGenerator("glyph") glyph2.appendComponent("a") glyph2.appendComponent("b") glyph2.appendComponent("b") is_compatible, report = glyph1.isCompatible(glyph2) self.assertFalse(is_compatible) self.assertEqual(report.componentDifferences, [(2, None, "b")]) self.assertFalse(report.componentOrderDifference) self.assertTrue(report.componentCountDifference) self.assertEqual(report.componentsMissingFromGlyph1, ["b"]) self.assertFalse(report.componentsMissingFromGlyph2) def test_isCompatible_components_disjoint(self): glyph1, _ = self.objectGenerator("glyph") glyph1.appendComponent("x") glyph2, _ = self.objectGenerator("glyph") glyph2.appendComponent("a") glyph2.appendComponent("a") glyph2.appendComponent("b") is_compatible, report = glyph1.isCompatible(glyph2) self.assertFalse(is_compatible) self.assertEqual( report.componentDifferences, [(0, "x", "a"), (1, None, "a"), (2, None, "b")] ) self.assertFalse(report.componentOrderDifference) self.assertTrue(report.componentCountDifference) self.assertEqual(report.componentsMissingFromGlyph1, ["a", "a", "b"]) self.assertEqual(report.componentsMissingFromGlyph2, ["x"]) def test_isCompatible_components_disjoint_equal_size(self): glyph1, _ = self.objectGenerator("glyph") glyph1.appendComponent("x") glyph1.appendComponent("y") glyph2, _ = self.objectGenerator("glyph") glyph2.appendComponent("a") glyph2.appendComponent("b") is_compatible, report = glyph1.isCompatible(glyph2) self.assertFalse(is_compatible) self.assertEqual(report.componentDifferences, [(0, "x", "a"), (1, "y", "b")]) self.assertFalse(report.componentOrderDifference) self.assertFalse(report.componentCountDifference) self.assertEqual(report.componentsMissingFromGlyph1, ["a", "b"]) self.assertEqual(report.componentsMissingFromGlyph2, ["x", "y"]) # ------------- # Interpolation # ------------- def test_interpolate_glyphWidth_without_rounding(self): interpolated, _ = self.objectGenerator("glyph") glyph_min, _ = self.objectGenerator("glyph") glyph_max, _ = self.objectGenerator("glyph") glyph_min.width = 1000 glyph_max.width = 2000 interpolated.interpolate(0.5154, glyph_min, glyph_max, round=False) self.assertEqual( interpolated.width, 1515.4 ) def test_interpolate_glyphWidth_with_rounding(self): interpolated, _ = self.objectGenerator("glyph") glyph_min, _ = self.objectGenerator("glyph") glyph_max, _ = self.objectGenerator("glyph") glyph_min.width = 1000 glyph_max.width = 2000 interpolated.interpolate(0.5154, glyph_min, glyph_max, round=True) self.assertEqual( interpolated.width, 1515 ) # --------------- # Transformations # --------------- def test_moveBy_only_contours(self): glyph = self.getGlyph_generic() glyph.moveBy((100, 0)) self.assertEqual( glyph.bounds[0], 200 ) glyph.moveBy((0, 250)) self.assertEqual( glyph.bounds[1], 240 ) # --- # API # --- def test_isEmpty_false_outlines(self): glyph = self.getGlyph_generic() self.assertFalse(glyph.isEmpty()) def test_isEmpty_true_clear(self): glyph = self.getGlyph_generic() glyph.clear() self.assertTrue(glyph.isEmpty()) def test_isEmpty_false_component(self): glyph = self.getGlyph_generic() glyph.clear() glyph.appendComponent("component 1") self.assertFalse(glyph.isEmpty()) def test_removeOverlap(self): glyph = self.getGlyph_generic() self.assertEqual(len(glyph), 2) glyph.removeOverlap() self.assertEqual(len(glyph), 1) def test_generator(test_name, metric, value): if '_invalid_' in test_name: def test(self): glyph = self.getGlyph_generic() with self.assertRaises(TypeError): setattr(glyph, metric, value) else: def test(self): glyph = self.getGlyph_generic() if '_set_' in test_name: setattr(glyph, metric, value) self.assertEqual( getattr(glyph, metric), value ) return test t_names = ('_get', '_set_valid_positive', '_set_valid_negative', '_set_valid_zero', '_set_valid_float', '_set_invalid_string', '_set_invalid_none') invalid = ('abc', None) metrics = { 'width': (250, 300, -485, 0, 101.5) + invalid, 'height': (750, 800, -10, 0, 801.5) + invalid, 'leftMargin': (100, 200, -15, 0, 201.5) + invalid, 'rightMargin': (50, 80, -20, 0, 81.5) + invalid, 'bottomMargin': (-10, 150, -35, 0, 151.5) + invalid, 'topMargin': (650, 750, -250, 0, 751.5) + invalid, } for i, t_name_suffix in enumerate(t_names): for metric_name, values in metrics.items(): test_name = 'test_{}{}'.format(metric_name, t_name_suffix) test = test_generator(test_name, metric_name, values[i]) setattr(TestGlyph, test_name, test) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_groups.py000066400000000000000000000210411477533125200245410ustar00rootroot00000000000000import unittest import collections class TestGroups(unittest.TestCase): def getGroups_generic(self): groups, _ = self.objectGenerator("groups") groups.update({ "group 1": ["A", "B", "C"], "group 2": ["x", "y", "z"], "group 3": [], "group 4": ["A"] }) return groups # ---- # repr # ---- def test_reprContents(self): font, _ = self.objectGenerator("font") groups = font.groups value = groups._reprContents() self.assertIsInstance(value, list) found = False for i in value: self.assertIsInstance(i, str) if "for font" in value: found = True self.assertTrue(found) def test_reprContents_noFont(self): groups, _ = self.objectGenerator("groups") value = groups._reprContents() self.assertIsInstance(value, list) self.assertEqual(value, []) # ------- # Parents # ------- def test_get_parent_font(self): font, _ = self.objectGenerator("font") groups = font.groups self.assertIsNotNone(groups.font) self.assertEqual( groups.font, font ) def test_get_parent_font_none(self): groups, _ = self.objectGenerator("groups") self.assertIsNone(groups.font) def test_set_parent_font(self): font, _ = self.objectGenerator("font") groups, _ = self.objectGenerator("groups") groups.font = font self.assertIsNotNone(groups.font) self.assertEqual( groups.font, font ) def test_set_parent_font_none(self): groups, _ = self.objectGenerator("groups") groups.font = None self.assertIsNone(groups.font) def test_set_parent_differentFont(self): font, _ = self.objectGenerator("font") fontB, _ = self.objectGenerator("font") groups, _ = self.objectGenerator("groups") groups.font = font self.assertIsNotNone(groups.font) with self.assertRaises(AssertionError): groups.font = fontB # --- # len # --- def test_len_initial(self): groups = self.getGroups_generic() self.assertEqual( len(groups), 4 ) def test_len_clear(self): groups = self.getGroups_generic() groups.clear() self.assertEqual( len(groups), 0 ) def test_len_add(self): groups = self.getGroups_generic() groups['group 5'] = ["D","E","F"] self.assertEqual( len(groups), 5 ) def test_len_subtract(self): groups = self.getGroups_generic() groups.pop('group 4') self.assertEqual( len(groups), 3 ) # --- # Get # --- def test_get_fallback_default(self): groups = self.getGroups_generic() self.assertEqual( groups.get("test"), None ) # ------- # Queries # ------- def test_find_found(self): groups = self.getGroups_generic() found = groups.findGlyph("A") found.sort() self.assertEqual( found, [u"group 1", u"group 4"] ) def test_find_not_found(self): groups = self.getGroups_generic() self.assertEqual( groups.findGlyph("five"), [] ) def test_find_invalid_key(self): groups = self.getGroups_generic() with self.assertRaises(TypeError): groups.findGlyph(5) def test_contains_found(self): groups = self.getGroups_generic() self.assertTrue("group 4" in groups) def test_contains_not_found(self): groups = self.getGroups_generic() self.assertFalse("group five" in groups) def test_get_found(self): groups = self.getGroups_generic() self.assertEqual( groups["group 1"], ("A", "B", "C") ) def test_get_not_found(self): groups = self.getGroups_generic() with self.assertRaises(KeyError): groups["group two"] # -------------- # Kerning Groups # -------------- def getGroups_kerning(self): groups = self.getGroups_generic() kerningGroups = { "public.kern1.A": ["A", "Aacute"], "public.kern1.O": ["O", "D"], "public.kern2.A": ["A", "Aacute"], "public.kern2.O": ["O", "C"] } groups.update(kerningGroups) return groups def test_side1KerningGroups(self): groups = self.getGroups_kerning() expected = { "public.kern1.A": ("A", "Aacute"), "public.kern1.O": ("O", "D") } self.assertEqual(groups.side1KerningGroups, expected) # self.assertEqual(super(groups, self)._get_side1KerningGroups(), expected) def test_get_side1KerningGroups(self): groups = self.getGroups_kerning() expected = { "public.kern1.A": ["A", "Aacute"], "public.kern1.O": ["O", "D"] } self.assertEqual(groups._get_side1KerningGroups(), expected) def test_side2KerningGroups(self): groups = self.getGroups_kerning() expected = { "public.kern2.A": ("A", "Aacute"), "public.kern2.O": ("O", "C") } self.assertEqual(groups.side2KerningGroups, expected) def test_get_side2KerningGroups(self): groups = self.getGroups_kerning() expected = { "public.kern1.A": ["A", "Aacute"], "public.kern1.O": ["O", "D"] } self.assertEqual(groups._get_side1KerningGroups(), expected) # ---- # Hash # ---- def test_hash(self): groups = self.getGroups_generic() self.assertEqual( isinstance(groups, collections.abc.Hashable), True ) # -------- # Equality # -------- def test_object_equal_self(self): groups_one = self.getGroups_generic() self.assertEqual( groups_one, groups_one ) def test_object_not_equal_other(self): groups_one = self.getGroups_generic() groups_two = self.getGroups_generic() self.assertNotEqual( groups_one, groups_two ) def test_object_equal_self_variable_assignment(self): groups_one = self.getGroups_generic() a = groups_one self.assertEqual( groups_one, a ) def test_object_not_equal_other_variable_assignment(self): groups_one = self.getGroups_generic() groups_two = self.getGroups_generic() a = groups_one self.assertNotEqual( groups_two, a ) # --------------------- # RoboFab Compatibility # --------------------- def test_remove(self): groups = self.getGroups_generic() groups.remove("group 2") expected = { "group 1": ("A", "B", "C"), "group 3": (), "group 4": ('A',) } self.assertEqual(groups.asDict(), expected) def test_remove_twice(self): groups = self.getGroups_generic() groups.remove("group 1") with self.assertRaises(KeyError): groups.remove("group 1") def test_remove_nonexistant_group(self): groups = self.getGroups_generic() with self.assertRaises(KeyError): groups.remove("group 7") def test_asDict(self): groups = self.getGroups_generic() expected = { "group 1": ("A", "B", "C"), "group 2": ("x", "y", "z"), "group 3": (), "group 4": ('A',) } self.assertEqual(groups.asDict(), expected) # ------------------- # Inherited Functions # ------------------- def test_iter(self): groups = self.getGroups_generic() expected = ["group 1","group 2","group 3", "group 4"] listOfGroups = [] for groupName in groups: listOfGroups.append(groupName) self.assertEqual(listOfGroups.sort(), expected.sort()) def test_iter_remove(self): groups = self.getGroups_generic() expected = [] for groupName in groups: groups.remove(groupName) self.assertEqual(groups.keys(), expected) def test_values(self): groups = self.getGroups_generic() expected = [("A", "B", "C"), ("x", "y", "z"),(),('A',)] self.assertEqual(groups.values().sort(), expected.sort()) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_guideline.py000066400000000000000000000716201477533125200251770ustar00rootroot00000000000000import unittest import collections from fontParts.base import FontPartsError class TestGuideline(unittest.TestCase): def getGuideline_generic(self): guideline, _ = self.objectGenerator("guideline") guideline.x = 1 guideline.y = 2 guideline.angle = 90 guideline.name = "Test Guideline" return guideline def getGuideline_fontGuideline(self): font, _ = self.objectGenerator("font") guideline = font.appendGuideline((1, 2), 90, "Test Guideline Font") return guideline def getGuideline_glyphGuideline(self): font, _ = self.objectGenerator("font") layer = font.newLayer("L") glyph = layer.newGlyph("X") guideline = glyph.appendGuideline((1, 2), 90, "Test Guideline Glyph") return guideline # ---- # repr # ---- def test_reprContents(self): guideline = self.getGuideline_generic() value = guideline._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_noGlyph(self): guideline, _ = self.objectGenerator("guideline") value = guideline._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_Layer(self): guideline = self.getGuideline_glyphGuideline() value = guideline._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) # -------- # Attributes # -------- # x def test_x_get_generic(self): guideline = self.getGuideline_generic() self.assertEqual( guideline.x, 1 ) def test_x_get_fontGuideline(self): guideline = self.getGuideline_fontGuideline() self.assertEqual( guideline.x, 1 ) def test_x_get_glyphGuideline(self): guideline = self.getGuideline_glyphGuideline() self.assertEqual( guideline.x, 1 ) def test_x_set_valid_zero_generic(self): guideline = self.getGuideline_generic() guideline.x = 0 self.assertEqual( guideline.x, 0 ) def test_x_set_valid_zero_fontGuideline(self): guideline = self.getGuideline_fontGuideline() guideline.x = 0 self.assertEqual( guideline.x, 0 ) def test_x_set_valid_zero_glyphGuideline(self): guideline = self.getGuideline_glyphGuideline() guideline.x = 0 self.assertEqual( guideline.x, 0 ) def test_x_set_valid_positive(self): guideline = self.getGuideline_generic() guideline.x = 1 self.assertEqual( guideline.x, 1 ) def test_x_set_valid_negative(self): guideline = self.getGuideline_generic() guideline.x = -1 self.assertEqual( guideline.x, -1 ) def test_x_set_valid_positive_float(self): guideline = self.getGuideline_generic() guideline.x = 1.1 self.assertEqual( guideline.x, 1.1 ) def test_x_set_valid_negative_float(self): guideline = self.getGuideline_generic() guideline.x = -1.1 self.assertEqual( guideline.x, -1.1 ) def test_x_set_valid_None(self): guideline = self.getGuideline_generic() guideline.x = None self.assertEqual( guideline.x, 0 ) def test_x_set_invalid_string(self): guideline = self.getGuideline_generic() with self.assertRaises(TypeError): guideline.x = "ABC" # y def test_y_get_generic(self): guideline = self.getGuideline_generic() self.assertEqual( guideline.y, 2 ) def test_y_get_fontGuideline(self): guideline = self.getGuideline_fontGuideline() self.assertEqual( guideline.y, 2 ) def test_y_get_glyphGuideline(self): guideline = self.getGuideline_glyphGuideline() self.assertEqual( guideline.y, 2 ) def test_y_set_valid_zero_generic(self): guideline = self.getGuideline_generic() guideline.y = 0 self.assertEqual( guideline.y, 0 ) def test_y_set_valid_zero_fontGuideline(self): guideline = self.getGuideline_fontGuideline() guideline.y = 0 self.assertEqual( guideline.y, 0 ) def test_y_set_valid_zero_glyphGuideline(self): guideline = self.getGuideline_glyphGuideline() guideline.y = 0 self.assertEqual( guideline.y, 0 ) def test_y_set_valid_positive(self): guideline = self.getGuideline_generic() guideline.y = 1 self.assertEqual( guideline.y, 1 ) def test_y_set_valid_negative(self): guideline = self.getGuideline_generic() guideline.y = -1 self.assertEqual( guideline.y, -1 ) def test_y_set_valid_positive_float(self): guideline = self.getGuideline_generic() guideline.y = 1.1 self.assertEqual( guideline.y, 1.1 ) def test_y_set_valid_negative_float(self): guideline = self.getGuideline_generic() guideline.y = -1.1 self.assertEqual( guideline.y, -1.1 ) def test_y_set_valid_None(self): guideline = self.getGuideline_generic() guideline.y = None self.assertEqual( guideline.y, 0 ) def test_y_set_invalid_string(self): guideline = self.getGuideline_generic() with self.assertRaises(TypeError): guideline.y = "ABC" # angle def test_angle_get_generic(self): guideline = self.getGuideline_generic() self.assertEqual( guideline.angle, 90 ) def test_angle_get_fontGuideline(self): guideline = self.getGuideline_fontGuideline() self.assertEqual( guideline.angle, 90 ) def test_angle_get_glyphGuideline(self): guideline = self.getGuideline_glyphGuideline() self.assertEqual( guideline.angle, 90 ) def test_angle_set_valid_zero_generic(self): guideline = self.getGuideline_generic() guideline.angle = 0 self.assertEqual( guideline.angle, 0 ) def test_angle_set_valid_zero_fontGuideline(self): guideline = self.getGuideline_fontGuideline() guideline.angle = 0 self.assertEqual( guideline.angle, 0 ) def test_angle_set_valid_zero_glyphGuideline(self): guideline = self.getGuideline_glyphGuideline() guideline.angle = 0 self.assertEqual( guideline.angle, 0 ) def test_angle_set_valid_positive(self): guideline = self.getGuideline_generic() guideline.angle = 10 self.assertEqual( guideline.angle, 10 ) def test_angle_set_valid_negative(self): guideline = self.getGuideline_generic() guideline.angle = -10 self.assertEqual( guideline.angle, 350 ) def test_angle_set_valid_positive_float(self): guideline = self.getGuideline_generic() guideline.angle = 10.1 self.assertEqual( guideline.angle, 10.1 ) def test_angle_set_valid_negative_float(self): guideline = self.getGuideline_generic() guideline.angle = -10.1 self.assertEqual( guideline.angle, 349.9 ) def test_angle_set_valid_positive_edge(self): guideline = self.getGuideline_generic() guideline.angle = 360 self.assertEqual( guideline.angle, 360 ) def test_angle_set_valid_negative_edge(self): guideline = self.getGuideline_generic() guideline.angle = -360 self.assertEqual( guideline.angle, 0 ) def test_angle_set_valid_None(self): guideline = self.getGuideline_generic() guideline.angle = None self.assertEqual( guideline.angle, 0 ) def test_angle_set_invalid_positive_edge(self): guideline = self.getGuideline_generic() with self.assertRaises(ValueError): guideline.angle = 361 def test_angle_set_invalid_negative_edge(self): guideline = self.getGuideline_generic() with self.assertRaises(ValueError): guideline.angle = -361 def test_angle_set_invalid_string(self): guideline = self.getGuideline_generic() with self.assertRaises(TypeError): guideline.angle = "ABC" def test_angle_set_valid_none_x0_y0(self): guideline = self.getGuideline_generic() guideline.x = 0 guideline.y = 0 guideline.angle = None self.assertEqual( guideline.angle, 0 ) def test_angle_set_valid_none_x1_y0(self): guideline = self.getGuideline_generic() guideline.x = 1 guideline.y = 0 guideline.angle = None self.assertEqual( guideline.angle, 90 ) def test_angle_set_valid_none_x0_y1(self): guideline = self.getGuideline_generic() guideline.x = 0 guideline.y = 1 guideline.angle = None self.assertEqual( guideline.angle, 0 ) def test_angle_set_valid_none_x1_y1(self): guideline = self.getGuideline_generic() guideline.x = 1 guideline.y = 1 guideline.angle = None self.assertEqual( guideline.angle, 0 ) # index def getGuideline_index(self): glyph, _ = self.objectGenerator("glyph") glyph.appendGuideline((0, 0), 90, "guideline 0") glyph.appendGuideline((0, 0), 90, "guideline 1") glyph.appendGuideline((0, 0), 90, "guideline 2") return glyph def test_get_index_noParent(self): guideline, _ = self.objectGenerator("guideline") self.assertIsNone(guideline.index) def test_get_index(self): glyph = self.getGuideline_index() for i, guideline in enumerate(glyph.guidelines): self.assertEqual(guideline.index, i) def test_set_index_noParent(self): guideline, _ = self.objectGenerator("guideline") with self.assertRaises(FontPartsError): guideline.index = 1 def test_set_index_positive(self): glyph = self.getGuideline_index() guideline = glyph.guidelines[0] with self.assertRaises(FontPartsError): guideline.index = 2 def test_set_index_negative(self): glyph = self.getGuideline_index() guideline = glyph.guidelines[1] with self.assertRaises(FontPartsError): guideline.index = -1 # name def test_name_get_none(self): guideline, _ = self.objectGenerator("guideline") self.assertIsNone(guideline.name) def test_name_set_valid(self): guideline = self.getGuideline_generic() guideline.name = u"foo" self.assertEqual(guideline.name, u"foo") def test_name_set_none(self): guideline = self.getGuideline_generic() guideline.name = None self.assertIsNone(guideline.name) def test_name_set_invalid(self): guideline = self.getGuideline_generic() with self.assertRaises(TypeError): guideline.name = 123 # color def test_color_get_none(self): guideline = self.getGuideline_generic() self.assertIsNone(guideline.color) def test_color_set_valid_max(self): guideline = self.getGuideline_generic() guideline.color = (1, 1, 1, 1) self.assertEqual(guideline.color, (1, 1, 1, 1)) def test_color_set_valid_min(self): guideline = self.getGuideline_generic() guideline.color = (0, 0, 0, 0) self.assertEqual(guideline.color, (0, 0, 0, 0)) def test_color_set_valid_decimal(self): guideline = self.getGuideline_generic() guideline.color = (0.1, 0.2, 0.3, 0.4) self.assertEqual(guideline.color, (0.1, 0.2, 0.3, 0.4)) def test_color_set_none(self): guideline = self.getGuideline_generic() guideline.color = None self.assertIsNone(guideline.color) def test_color_set_invalid_over_max(self): guideline = self.getGuideline_generic() with self.assertRaises(ValueError): guideline.color = (1.1, 0.2, 0.3, 0.4) def test_color_set_invalid_uner_min(self): guideline = self.getGuideline_generic() with self.assertRaises(ValueError): guideline.color = (-0.1, 0.2, 0.3, 0.4) def test_color_set_invalid_too_few(self): guideline = self.getGuideline_generic() with self.assertRaises(ValueError): guideline.color = (0.1, 0.2, 0.3) def test_color_set_invalid_string(self): guideline = self.getGuideline_generic() with self.assertRaises(TypeError): guideline.color = "0.1,0.2,0.3,0.4" def test_color_set_invalid_int(self): guideline = self.getGuideline_generic() with self.assertRaises(TypeError): guideline.color = 123 # identifier def test_identifier_get_none(self): guideline = self.getGuideline_generic() self.assertIsNone(guideline.identifier) def test_identifier_generated_type(self): guideline = self.getGuideline_generic() guideline.getIdentifier() self.assertIsInstance(guideline.identifier, str) def test_identifier_consistency(self): guideline = self.getGuideline_generic() guideline.getIdentifier() # get: twice to test consistency self.assertEqual(guideline.identifier, guideline.identifier) def test_identifier_cannot_set(self): # identifier is a read-only property guideline = self.getGuideline_generic() with self.assertRaises(FontPartsError): guideline.identifier = "ABC" def test_identifier_force_set(self): identifier = "ABC" guideline = self.getGuideline_generic() guideline._setIdentifier(identifier) self.assertEqual(guideline.identifier, identifier) # ------- # Methods # ------- def getGuideline_copy(self): guideline = self.getGuideline_generic() guideline.name = "foo" guideline.color = (0.1, 0.2, 0.3, 0.4) return guideline # copy def test_copy_seperate_objects(self): guideline = self.getGuideline_copy() copied = guideline.copy() self.assertIsNot(guideline, copied) def test_copy_same_name(self): guideline = self.getGuideline_copy() copied = guideline.copy() self.assertEqual(guideline.name, copied.name) def test_copy_same_color(self): guideline = self.getGuideline_copy() copied = guideline.copy() self.assertEqual(guideline.color, copied.color) def test_copy_same_identifier(self): guideline = self.getGuideline_copy() copied = guideline.copy() self.assertEqual(guideline.identifier, copied.identifier) def test_copy_generated_identifier_different(self): guideline = self.getGuideline_copy() copied = guideline.copy() guideline.getIdentifier() copied.getIdentifier() self.assertNotEqual(guideline.identifier, copied.identifier) def test_copy_same_x(self): guideline = self.getGuideline_copy() copied = guideline.copy() self.assertEqual(guideline.x, copied.x) def test_copy_same_y(self): guideline = self.getGuideline_copy() copied = guideline.copy() self.assertEqual(guideline.y, copied.y) def test_copy_same_angle(self): guideline = self.getGuideline_copy() copied = guideline.copy() self.assertEqual(guideline.angle, copied.angle) # transform def getGuideline_transform(self): guideline = self.getGuideline_generic() guideline.angle = 45.0 return guideline def test_transformBy_valid_no_origin(self): guideline = self.getGuideline_transform() guideline.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual(guideline.x, -1) self.assertEqual(guideline.y, 8) self.assertAlmostEqual(guideline.angle, 56.310, places=3) def test_transformBy_valid_origin(self): guideline = self.getGuideline_transform() guideline.transformBy((2, 0, 0, 2, 0, 0), origin=(1, 2)) self.assertEqual(guideline.x, 1) self.assertEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 45.000, places=3) def test_transformBy_invalid_one_string_value(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.transformBy((1, 0, 0, 1, 0, "0")) def test_transformBy_invalid_all_string_values(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.transformBy("1, 0, 0, 1, 0, 0") def test_transformBy_invalid_int_value(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.transformBy(123) # moveBy def test_moveBy_valid(self): guideline = self.getGuideline_transform() guideline.moveBy((-1, 2)) self.assertEqual(guideline.x, 0) self.assertEqual(guideline.y, 4) self.assertAlmostEqual(guideline.angle, 45.000, places=3) def test_moveBy_invalid_one_string_value(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.moveBy((-1, "2")) def test_moveBy_invalid_all_strings_value(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.moveBy("-1, 2") def test_moveBy_invalid_int_value(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.moveBy(1) # scaleBy def test_scaleBy_valid_one_value_no_origin(self): guideline = self.getGuideline_transform() guideline.scaleBy((-2)) self.assertEqual(guideline.x, -2) self.assertEqual(guideline.y, -4) self.assertAlmostEqual(guideline.angle, 225.000, places=3) def test_scaleBy_valid_two_values_no_origin(self): guideline = self.getGuideline_transform() guideline.scaleBy((-2, 3)) self.assertEqual(guideline.x, -2) self.assertEqual(guideline.y, 6) self.assertAlmostEqual(guideline.angle, 123.690, places=3) def test_scaleBy_valid_two_values_origin(self): guideline = self.getGuideline_transform() guideline.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual(guideline.x, 1) self.assertEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 123.690, places=3) def test_scaleBy_invalid_one_string_value(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.scaleBy((-1, "2")) def test_scaleBy_invalid_two_string_values(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.scaleBy("-1, 2") def test_scaleBy_invalid_tuple_too_many_values(self): guideline = self.getGuideline_transform() with self.assertRaises(ValueError): guideline.scaleBy((-1, 2, -3)) # rotateBy def test_rotateBy_valid_no_origin(self): guideline = self.getGuideline_transform() guideline.rotateBy(45) self.assertAlmostEqual(guideline.x, -0.707, places=3) self.assertAlmostEqual(guideline.y, 2.121, places=3) self.assertAlmostEqual(guideline.angle, 0.000, places=3) def test_rotateBy_valid_origin(self): guideline = self.getGuideline_transform() guideline.rotateBy(45, origin=(1, 2)) self.assertAlmostEqual(guideline.x, 1) self.assertAlmostEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 0.000, places=3) def test_rotateBy_invalid_string_value(self): guideline = self.getGuideline_transform() with self.assertRaises(TypeError): guideline.rotateBy("45") def test_rotateBy_invalid_too_large_value_positive(self): guideline = self.getGuideline_transform() with self.assertRaises(ValueError): guideline.rotateBy(361) def test_rotateBy_invalid_too_large_value_negative(self): guideline = self.getGuideline_transform() with self.assertRaises(ValueError): guideline.rotateBy(-361) # skewBy def test_skewBy_valid_no_origin_one_value(self): guideline = self.getGuideline_transform() guideline.skewBy(100) self.assertAlmostEqual(guideline.x, -10.343, places=3) self.assertEqual(guideline.y, 2.0) self.assertAlmostEqual(guideline.angle, 8.525, places=3) def test_skewBy_valid_no_origin_two_values(self): guideline = self.getGuideline_transform() guideline.skewBy((100, 200)) self.assertAlmostEqual(guideline.x, -10.343, places=3) self.assertAlmostEqual(guideline.y, 2.364, places=3) self.assertAlmostEqual(guideline.angle, 5.446, places=3) def test_skewBy_valid_origin_one_value(self): guideline = self.getGuideline_transform() guideline.skewBy(100, origin=(1, 2)) self.assertEqual(guideline.x, 1) self.assertEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 8.525, places=3) def test_skewBy_valid_origin_two_values(self): guideline = self.getGuideline_transform() guideline.skewBy((100, 200), origin=(1, 2)) self.assertEqual(guideline.x, 1) self.assertEqual(guideline.y, 2) self.assertAlmostEqual(guideline.angle, 5.446, places=3) # ------------- # Normalization # ------------- # round def getGuideline_round(self): guideline = self.getGuideline_generic() guideline.x = 1.1 guideline.y = 2.5 guideline.angle = 45.5 return guideline def test_round_close_to(self): guideline = self.getGuideline_round() guideline.round() self.assertEqual(guideline.x, 1) def test_round_at_half(self): guideline = self.getGuideline_round() guideline.round() self.assertEqual(guideline.y, 3) def test_round_angle(self): guideline = self.getGuideline_round() guideline.round() self.assertEqual(guideline.angle, 45.5) # ---- # Hash # ---- def test_hash_object_self(self): guideline_one = self.getGuideline_generic() self.assertEqual( hash(guideline_one), hash(guideline_one) ) def test_hash_object_other(self): guideline_one = self.getGuideline_generic() guideline_two = self.getGuideline_generic() self.assertNotEqual( hash(guideline_one), hash(guideline_two) ) def test_hash_object_self_variable_assignment(self): guideline_one = self.getGuideline_generic() a = guideline_one self.assertEqual( hash(guideline_one), hash(a) ) def test_hash_object_other_variable_assignment(self): guideline_one = self.getGuideline_generic() guideline_two = self.getGuideline_generic() a = guideline_one self.assertNotEqual( hash(guideline_two), hash(a) ) def test_is_hashable(self): guideline_one = self.getGuideline_generic() self.assertTrue( isinstance(guideline_one, collections.abc.Hashable) ) # ------- # Parents # ------- def test_get_parent_font(self): font, _ = self.objectGenerator("font") layer = font.newLayer("L") glyph = layer.newGlyph("X") guideline = glyph.appendGuideline((0, 0), 90, "Test Guideline") self.assertIsNotNone(guideline.font) self.assertEqual( guideline.font, font ) def test_get_parent_noFont(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") guideline = glyph.appendGuideline((0, 0), 90, "Test Guideline") self.assertIsNone(guideline.font) def test_get_parent_layer(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") guideline = glyph.appendGuideline((0, 0), 90, "Test Guideline") self.assertIsNotNone(guideline.layer) self.assertEqual( guideline.layer, layer ) def test_get_parent_noLayer(self): glyph, _ = self.objectGenerator("glyph") guideline = glyph.appendGuideline((0, 0), 90, "Test Guideline") self.assertIsNone(guideline.font) self.assertIsNone(guideline.layer) def test_get_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") guideline = glyph.appendGuideline((0, 0), 90, "Test Guideline") self.assertIsNotNone(guideline.glyph) self.assertEqual( guideline.glyph, glyph ) def test_get_parent_noGlyph(self): guideline, _ = self.objectGenerator("guideline") self.assertIsNone(guideline.font) self.assertIsNone(guideline.layer) self.assertIsNone(guideline.glyph) def test_set_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") guideline = self.getGuideline_generic() guideline.glyph = glyph self.assertIsNotNone(guideline.glyph) self.assertEqual( guideline.glyph, glyph ) def test_set_parent_glyph_none(self): guideline, _ = self.objectGenerator("guideline") guideline.glyph = None self.assertIsNone(guideline.glyph) def test_set_parent_font_none(self): guideline, _ = self.objectGenerator("guideline") guideline.font = None self.assertIsNone(guideline.glyph) def test_set_parent_glyph_exists(self): glyph, _ = self.objectGenerator("glyph") otherGlyph, _ = self.objectGenerator("glyph") guideline = glyph.appendGuideline((0, 0), 90, "Test Guideline") with self.assertRaises(AssertionError): guideline.glyph = otherGlyph def test_set_parent_glyph_font_exists(self): guideline = self.getGuideline_fontGuideline() glyph, _ = self.objectGenerator("glyph") with self.assertRaises(AssertionError): guideline.glyph = glyph def test_set_parent_font_font_exists(self): guideline = self.getGuideline_fontGuideline() font, _ = self.objectGenerator("font") with self.assertRaises(AssertionError): guideline.font = font def test_set_parent_font_glyph_exists(self): guideline = self.getGuideline_glyphGuideline() font, _ = self.objectGenerator("font") with self.assertRaises(AssertionError): guideline.font = font # -------- # Equality # -------- def test_object_equal_self(self): guideline_one = self.getGuideline_generic() self.assertEqual( guideline_one, guideline_one ) def test_object_not_equal_other(self): guideline_one = self.getGuideline_generic() guideline_two = self.getGuideline_generic() self.assertNotEqual( guideline_one, guideline_two ) def test_object_equal_self_variable_assignment(self): guideline_one = self.getGuideline_generic() a = guideline_one a.x = 200 self.assertEqual( guideline_one, a ) def test_object_not_equal_other_variable_assignment(self): guideline_one = self.getGuideline_generic() guideline_two = self.getGuideline_generic() a = guideline_one self.assertNotEqual( guideline_two, a ) # --------- # Selection # --------- def test_selected_true(self): guideline = self.getGuideline_generic() try: guideline.selected = False except NotImplementedError: return guideline.selected = True self.assertEqual( guideline.selected, True ) def test_not_selected_false(self): guideline = self.getGuideline_generic() try: guideline.selected = False except NotImplementedError: return self.assertEqual( guideline.selected, False ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_image.py000066400000000000000000000756071477533125200243250ustar00rootroot00000000000000import unittest import collections from fontParts.base import FontPartsError testPNGData = """ 89504e470d0a1a0a0000000d4948445200000080000000800806000000c33e61cb0000 02ee694343504943432050726f66696c65000078018554cf6b134114fe366ea9d02208 5a6b0eb27890224959ab6845d436fd11626b0cdb1fb64590643349d66e36ebee26b5a5 88e4e2d12ade45eda107ff801e7af0642f4a855a4528deab2862a1172df1cd6e4cb6a5 eac0ce7ef3de37ef7d6f76df000d72d234f58004e40dc752a211696c7c426afc88008e a20941342555dbec4e2406418373f97be7d87a0f815b56c37bfb77b277ad9ad2b69a07 84fd40e0479ad92ab0ef17710a591202883cdfa129c77408dfe3d8f2ec8f394e7978c1 b50f2b3dc459227c40352dce7f4db853cd25d34083483894f571523e9cd78b94d71d07 696e66c6c810bd4f90a6bbcceeab62a19c4ef60e90bd9df47e4eb3de3ec221c20b19ad 3f46b88d9ef58cd53fe261e1a4e6c4863d1c1835f4f86015b71aa9f835c2145f104d27 a25471d92e0df198aefd56f24a82709038ca646180735a484fd74c6ef8ba87057d26d7 13afe2775651e1798f1367ded4ddef45da02af300e1d0c1a0c9a0d48501045046198b0 5040863c1a3134b2723f23ab061b937b3275246abb746244b1417b36dc3db751a4dd3c fe2822719443b50892fc41fe2aafc94fe579f9cb5cb0d856f794ad9b9abaf2e03bc5e5 99b91a1ed7d3c8e3734d5e7c95d47693574796ac797abc9aec1a3fec579731e682358f c473b0fbf12d5f95cc97298c14c5e355f3ea4b84755a3137df9f6c7f3b3de22ecf2eb5 d673ad898b37569b9767fd6a48fbeeaabc93e655f94f5ef5f1fc67cdc463e229312676 8ae7218997c52ef192d84bab0be2606dc7089d958629d26d91fa24d560609abcf52f5d 3f5b78bd467f0cf5519419ccd25489f77fc22a64349db90e6ffa8fdbc7fc17e4f78ae7 9f28022f6ce0c899ba6d5371ef10a165a56e73ae0217bfd17df0b66e6ba37e38fc0458 3cab16ad52359f20bc011c76877a1ee82998d39696cd3952872c9f93bae9ca6252cc50 db435252d725d7654b16b3995562e976d899d31d6e1ca13942f7c4a74a6593faaff111 b0fdb052f9f9ac52d97e4e1ad68197fa6fc0bcfd45c0788b8900000009704859730000 0b1300000b1301009a9c1800001ea3494441547801ed9d578c1dc59ac77b926d8c0360 9c6dec019383c0cb72612f4164010ff000175878602584001184b80fc003d24a2b2178 4144ed5e5602b1c2c6d2458056225d0c0f5c049820e20513cc1a6430c9018fd3cc9c99 fd7e55e73fa74ebb4fe83e3d33a7614aaa53d515beaafabe7f7d15baba4e4794a3e9e8 e888bacc24901cb6b00e850f0ff3586dc85b2b9c94f138d22785139644ab5e7a4728f9 a7aade9664786868c8aa92d080e4fc6d1f3a22948c35557e5cf94b196915251b18ef36 20940c074356e910cda1bf10ede9cea1969dd6bbba8c19a579f3e61d76fbedb7ff47d0 43048aaa62d41b2d9078c7343a552c9c3c23a4ca71a2572b7c244f9c1619ccb8322c4e 74460a505c9940a8714cd643c33ffffcf386b7df7efbef6bd6acf9fbb66ddbbeefecec dcab9c76c05c0161a43dc415c18c30224365c98b355e744e3626f51f71c41167bcfbee bb2fc0b0725c5db2301d13c8c33d670d8fd3129d303c0c7385957faccece17c613265b 2a958677eddad5b16eddba0dcf3cf3cce38f3efae8fd0684cdd6f61e4bb3db320b0434 ca37ac4cbb9d9d3c00d06d4c98624c183af4d043cf58bd7af5b3e61db446b7427b4c79 86d025f8d0b576380058d8b00180349db46bafbdf6eaf8e4934ffe71c30d375cb97efd fa7fd89030c5e2775a1c20088130a6edc85258ab4380d300563074f04fe9eeee86694c 040b0500abaf13362e06e1a399b0089fb92d6e7f7fffd0d6ad5b070f38e0802356ae5c f9bf575d75d5659f7ffef9bb163fb50c82b0f7877e4fb8cd7e41741623e1e26211788f d949660b6fd00249165098e99c3c7972cfe6cd9b4b93264d5ab862c58abf2e5bb6ec04 13fe6e4060f17406a729cc159fccdb9e262b00688d1a2710d0f056354a5b7009e1cbe0 470b8480000826fcae9f7efaa9d4d3d333d740b0aab7b7f70f0108e8108500412b0010 8f040068e5414f74c7cd0d27a5a15f15020c80c03441d7d75f7f5d9a356bd65c9b14ae 3cf0c003ffb96820685560712dc0927064ec2cbadf26b72c711ab5c781c034c0bc871f 7ef8c93208facbc341db6b825601a04e812b30846185f40bb8aabc9e050601837826bd bffefa6b575f5f5f69c992258060e5d2a54bffc934412140d02a002a83a5b855601741 874682972bc1872e0018181888b66fdf4e6f2f2d5cb870fe430f3db4d2c0b0bc082068 150021bf0aeb97e0e5d210848c91f0e5128e3f04017e5b1544d3a64deb3230a009163c f0c0032b162f5e7cac8160c0e2591db4e57030010027662f68bc0853067f28ecd04f1c bd1fd7560291ed0aba6c3366cce8b2bd8292cd0916dd7ffffd2b4d231c6313c610049a 3457ab1b153ac66ea5b5635c703b1787a031b8718bc043cb061100601830c1477befbd f78826b0fd81c5f7dd77df8af9f3e71f5d0601ef0fb4692620b8b2c6eb275700c49955 b467044b9de5cacf3382c6ca2f97303401c6de154453a74e8da64c99e2408026b0edf1 2580c05e941d692018b47c6d05825c01305e28ceb35c848e41c09810c484c90a0c3ccb bf63c70ef6061c0000c2f4e9d3dd7070f8e1872f35103c3977eedcb603c104009c98ab 7f0402b92110e417107049878b06401b0002acbd341a0181bd2905042be7cc9973443b 6982dc0110f698a2fb43e1aa2deaed02809e7177efdeedc060dbc4239a0010b03a6038 38f2c8237bdb0d04b903a0ba2f15f70981631034460048724903000607790bee378798 186a38084170f4d1471f5806c1e1eda0092600e04456ff476008854f8ef019bfadf9dd 3b02c0c0502010000081c0b444a99d40d0360008dfc0d517c7d8c54af0127658b2e204 02b480f568f7d690309e1b80e0a076d0046d0100bd6a0d19dcae7e095ca0d0332e4640 ae07025607654d70109b4536311cb7e1207700840c69d62f00349bbe1dd3d1e3a9978c ea584b13b40b087207807a8018d1c88551a84e265021031be51bcb780953f5d33375c0 8f90e300509ce2c3e1808d22e60402c131c71c336e9a20770064110c9327cda0b3e41f af3c714030f9939156d3336989d7c430dc27b0fd831220b01748633e1ce40e001a9ac6 d273a401d48bd2e46f97b4d41d01e362c27a290ee1cbb257801fd7de1fb8394119042b 66cf9e1d9f134094f1a532c650480e267700a4a913bd0446a1017899823fed1092a6bc 56d3523f99b85f82b7c3211c1089366dda14fdf2cb2fce6ed9b2c585eddcb9d3b58fde 3f73e6ccc804edecbefbee1bf116d1b6924b471d75d4b2071f7c70854d0c0f0bf60946 ed55725b1ce294061073dbdbf51a8e3a02020181decf56f0cb2fafb657c3bf4676749c 43220ed84a4b1ae6028cfff8ed055164ef07a259b3f63740cc88162c58e04e162d5fbe 7c19ab831b6fbcf1723b78fa99816b2fe311df1d840770423f456432e30e00188806c0 8a99995a3246993a3a3cdf257c5c7a7f67a71fe3bbbbfd30e0e3a9d4b0b5cd7f60421b 013bee0f3ffc605bc47b1b207a1c186c4218d96be3a8b7b7b76bc182f9a5934f3e79d9 238f3cf2e4d5575f7da97d96f6b995c1d757141e7e7bd93208c60c00c1496b53835e5a b8c60fb3fe94adf7d3b37c7c3bfc8675f5f565d5a27a5343862d2acc32d0dbce4e40d0 65cfde020a86b6ee6e1a4e5b87590198da9fe9e6010082b3045f7ef965641f99988698 dab570e10203c12907fde52ffff53f575df56f97d8b0f2ad816ab2d1e133344000310a 2e73d37c194cee0000f99890715ec05ed0d6d6b2c049336c8c2a99ea1c3255391c6dd9 420fa16ded8300cd49bc2ba0b26c1d7675c6b577402640de067698cb3b01be8e9a64ed c3f29189638903361a808f8ae9f97c4b333cdce380d2d3c31b44df6e3b4516ad5bf77f 5d6bd77e513af1c43f1c7ed75d77fdf7adb7fef9f25dbb766e31fef6184d8fa416854f ad7207806f6abddfeaeff03c83aabfc449caeddb9c14d37c98c0d94c0e951777112080 aed4bb428d5e6f5f8ebb993de33cf2e9e90104ee8b2297d0e71f32a0f4b9c323dddd8a 67422c5a936d653095f943d75b6fbd3578f0c1cb4ebceeba6befbef7de7bafb7144cdc 2108084438b316c81d00494c26acbc3ab23a7b158f1aedec1c760c83698ca32c890607 2b132b97b8fcc392a9c2a030a6393f424bb3d7c024adabcb2f924261ab0733aeb37219 1c1c30b7dfb400cbbacad20e3fb3fd818141038c7f47e0e9f8f9c0c0c0761b02f6b13c 930d201ad6d5b149efe745364fe8deb8f187d2e2c58bff74d65967ad79f9e597ffb33c 1f1008320b1fce8d0a002a0c6302e41bd2df0fa33837873b688cf3e1a8fb81815d4ef5 6fd8f085eb59a148010ff93efae8a391f7ed617c337e04c0b2cbdec7ef413f9e9fba33 535fbb766df4e38f3fba091a6115a3decf10266dc0c40ebf9fe4d1b669d3a6ba3219db bdc6505a2fe42953265b9e5d4683eb05e80c74123fa10478a6e9cd4e72342d6de7ae5d bb2303c09f3ff8e083576d65f095f1a5dbea158e9761252bd56de0cb1d006216bdd5f7 581ae3fd3d3d9dd62bba1d28c4147afed6ad8ca3fdd1fefbcfb0064babc16034445764 1f5d441f7ef88e130c9a406534689b8b863ecb310070da692755d14fca0f6db66a5f7a e90b1b87d745acd10171c5a0a12a4ff842ad077858f7b3bc3bf2c8435dd9e007ba00c6 fb876c9f60b36980e96e12e8dbe3014087080dfc30dbc16ea1d56bded9679ffdaff62d e2bf5b99932c1f07103ca2c24c29fcb90340657ba6b034f20724085758c840040a183a 3afa6c1dbcd0f5769fce330d86b2b1326dda7427448609cf309554df05400804a1b2cc a2a7d633309cf37cccd0599f23c8a4a10341568c7fa05e6acff4e933a2850b1739f0aa beb8f203f879f3e633e3b7f6bbbb071c392f70269b5e43f20c00ad4ec6968ec8f608ce 5cb56ad5fd16d66719b443485e9053552b021b99dc0120e18505abd17215c733e97da3 fd0b21982d1a4a4f184c409d62142e3af55c00c0100270341ed74b4f5d484f797ec8ea 4f04403d1a6a03601060a9b3da8bcbfc00cb5040998a87aef84198fc46b383b61820e7 1b98f733adb6c3f8c4d53cba8c23b5f0292b770040346e24d07838cfc42559a5ad17a7 34f55ce5270dc3014cad67c2f4f2e3366b9496b2d03a8001132f973859c5e18656c227 0c3a00ca76183bcdcf3d0ce1f6b02b23cbcf9800204bc58a9e87deea570195771c1234 2041330800085a466908c32f10e0428f7c0600e416022093faa7ccdc01a01ea0063572 499f64954f713cc31059c53772c3f421ad5af994065779719b35ca437e8485152dd120 0dbd59360480d28a8e5cd20018e85918c247fd877300914fe5e60e8054a5ff46132334 3400026612cb7080215c46c247a0cc374213a6c32f2d409e32a0e8f1dcc5d011a60d69 34eb2f0c005a6d68b30cc9331dc24768f46a19dac1b3e242809046ed0c5dfc804000b0 64101451b9644f6d0a03005a0623649b6d69daf471ba69f32b3d42a6e7cb121e1ac5a3 29428090264c2b3f00a0f793de4c5ce8f167d234657205000d8937a6512d9427ee2a5f 182ee68a294a53cf559e904ebdf4a40b55368ccf5a1ec2c28a9eca859ee2702953262c cb63a6029c182d3255328a404a375700345376d056973c7cc62f2b5a7a0e19a3b874ae 7fd9129697945fe5299ddca4b4f5c204246df156a7f51d852ddf247054a7f54fd063b8 0004799adc0110a259150db59f75a8114338f31fc5e3c78ae984e3d71c0910c88e1069 e0f15bcee48b6cf9c4264bfd0cc4938e77fc94a5fcf573556243a042cb76712d72cf8e 8ae07d19fe3c81da9cc43fab8dd312c425c757ca4febcb1d00aa000cc7e0c2448488eb 995271bbba3a46ce036cddcae9e0cac9209849bcddbfe4e8646bbc671ee5fffaab7f33 e76b96fccb7e3d3b860303fea6d0e454f54311a66d26daa64dc9de0594ac9797991164 830f7d7dec7efa6d60f2b058a0bdb85e73789776fbb03d354699e49e050465d5f38e1a 006a174a5d7d7de3bd19801057014f758f57fab097d52ec7c7280f4fa1bf56beea34d5 e5d7ca138657f2371e72103a16237fe599deae70b4844f97f7ef9800c0ab2e1ae3911c 36d89f056022e4efda41fdaaa7c34ce2fde9998a00096fd64820d0e4f0853f71543b37 1a8074d44779b395c73171cefbf558eff5fb0061a9d487b6110f7dce1a227092e2ca2a 0fe9651596879b3b0068ac9f39fbd9336fdf5073fec54ae54c0061fe2d58edf3003086 49cf8e1ddb1dcdac0d469d0e0cec8ebefffeab86742873d2a4c9362c6d3786ef29b846 75203f278376eedc166dd8f0a5b93b1201502a0d46f6170416d7ed844d3b393b386992 df21c4c58633ff4200000660c2ca826626af9c072895380f404ff40724008cbded7587 3d66cf9e59252001a0af8fa554f6ad598616981b3f6f90244ccae40d1de716b404549b 92d2c7c3484bbec993274573e6ccb439406527304cbb71e3ce689f7df6766063d8830f 582f70bf7cf47edff3cb6c0d49e4e2cf5d0320788c5c1a21e3c3fcd866292cd8ef897b 066f73efcfd114ca4b384b1fced8c31c9e6545b3914b7a04c22e1ae70de2dbaef1fca4 f5173ded950900d0130ddef7736f10758f9b9f7ffec500399bdb43ac4e2c4d7cc7a1be 98b80bcd902f2e510e3fb903205e27358470fcf167c2691ce1083f0e00c279151ae623 4f5a23facd00803a50a7560cf975a62009006a57bc2c09b995b2d3e41d7500c42b136f 60f88c5f56f9c2679826abf8466e983ea4552b5f3c4d98bf569e303c4c2f5ab849a651 7c521ec26ad1ab95be5ef89ebaa95eea89b8df1c0772d70069d119f682d02f4e2b8c67 f52edc664d9836a4552bbfd2e0b6525e48077fdc348aaf973e6c533c5ddae7dc0190b6 0269d2671508f99284d0a8ec56ca6b44bb5de27307405a46c77b829ec5a0f8b3c2b3b8 cdd06a264db3658b166edc847149f1f5d2c7e35a79ce1d00ad54a651deb1ee9159cad3 8aa6515bda253e77003483e6b0f1f19ea067a5893f2b3c8bdb0cad66d2a429bb163d85 cb6d44b3d9748de8c4e3730740bc803c9fe95d597a1879d21a188e065099cde6573edc 229889656011a4348a752c9406c832269327abc95a5e2b6566ad6bd67cbf0b0d502481 641564d67cb96a802c1315e589bb6a50389666ed9121ad909ec24357f55098cad47323 57e9f98a0713a7a7fc0a97abf05aaed2e1e6697205409e15132d18ca9b3cde0a720b97 18acf846aed267619cf2e2366b48cbc451006836df78a5cb1d0069192d64c300f94503 1786ea1b3aee09287f1ad534bf2444ad0444bb1601c5ab6ce5af953e1e2e0070091406 3aa2194fabb85af1617aa5911bc6b5e26ffb39000c4503d8458aeeb57096c6f23a964b 1ac357cdb5e8c060ca44dbc85f2b6d523879c9c7c512453085000002e4564dce056060 72b3969ecff0a11b3c39a042de5a06e101166ef9245fb3e5281d6700c8b7fffefbbbbc d0cbd3402f4f9a6d0f00316fd1a245ee808518ddac0b00103a7fecf8d5575f39bf8603 d1964b3843cc77df7d17d93d3cce8f409b2d0b3a68194ef9008046874f54ee78bab903 4008cdcbd5699aa54b978e08240bc310ecabafbeea26680c2971c1227cd579f5ead54e db84c7d99a2993fc0c1d5c45b3cf3efbb832a8bfe8b6eaaa0e00322f933b00f2aa98e8 c0347a151a8061201c9b9bed99089b5ef9c5175f448f3ffeb8d302fedcdf14375b6792 c9bd7eb8cf3df75cf4c61b6f8ccc399a2d4342e11a1bfb9f40277485a92d79b8f0234f d336ab8078ef081b49ef44385cf3f6d24b2fb9439b617c337e687053185a60fdfaf591 ddc51bd9dd7b8e96fd7d4bb471e3462778aea363064f7dd20a90390acb3ffb53285725 f5fea4fad56b6fadf449e1ad86e50e80562b542fff89279e18bdf2ca2b2393c17a696b c5a19abffdf6dbe8b1c71e73430a6a1e700002266f8004c11396c6206c96a9871c7248 d4dbdbeb2eb4cabbb7529fbc691602003017d56a7fc61cd93f70469f7cf289eba56985 0403190e50fffcc9733849230c938526f9000d43d519679cc1a37bce5b588e70ce3f6d 3f07507ba58ecf3ffffc4c3d5474701172287c8565153e00659571d8618745c71f7fbc 032b614530c5a8a5711286a2a6d100a79e7a6ab479f3e63d3e160124636d11b2f6272e bef8e2913a8da6f0f3d42c8501000ca5e1f4dc4b2fbd343ae080035caf0318632df4b0 3cea04182fbcf0c2c8fe2ade81b428bd1f9ee60e0018325a16c60200c6efebafbfde2d ddf8f42aed7a9d86e76128971dc3d34e3b2dbae8a28bdc1c80b0d16a3f7586769e2677 00e459b9245a808009214bb85b6eb9c52d0f197fc752134808f6572e6e39697febb2c7 9c22a9eeed18963b00eaa1bf1e03c4d47a691427101c7cf0c1d11d77dce13689e889a8 66e246cb5047e8f3ae80770ba8fd6bafbdd6f54a269069dad0282df171ab76d1cebccc a82f032b0df59743549efd528986c05485b31697bf512361c4c2850ba33befbc337af6 d967a3bffded651b8377b8cd185471385637a29514af7ae89e0096794c44172f5e145d 7ef9e591fdc5db08e8d20e43d0260f6d8f8356f5a64e5ed6f9093cdece5103801ac198 8d8579b2fccb46b814dbb973979b49f3a749e44b63b8d10346fef18fff62d7bbef17bd fefaeb6ea7afaf6f9bbd46f657b50a60213343bf2f8f1e57f6953dfe6e23fee4823f7d 1872fbfbc71fbf3c3ae59453dc86d1dab59f9585977e5c86e637df7c6375f480a78eb4 83f714b2fa4731e2c417802360a6e153adb4a306800a5351c9085545c12c7a273b6da8 4de2ed06274bd3d1c1664cba1d3864c5a6ddb66d25db283ac666e2c7b937796bd77e1a 7dfae947b6c5bbc12e69e8b3340c0fbed7516635232bcb47fdf30700f13b83fbdad2f3 40db863e3662878f092843405fdfa0096c9ad5379be1ff11ec4fc02cb33f3ae6e7e3f0 82fb14e0159667f85501a73de46a24955c897a62bed25c97c2ed1c066243b617160283 c108857b7230bb766db357a80b9c26c88a70699ddede7d0d0c87da5fac5c60d7b0fc68 57b57c63d7c36c70fead5b37bb2b6758bba389bc06e1ae9e6e37744c9b36c30e73ec67 f7f2cfb7b77a8bcd2eb29e3fd381a7bfdfff25cc94299cf649dfeb5d43cb3f7d7d9bec 85d36c5bc9ecedea0090b1a6041cafe01756e1d200218d3cfca30880b07af43084ce1e bbae8ce3998b21f8c30818eb2f8240285901a012eddf55cc8b46e9b46b5ae6bb9b41a4 2906061892fc70c43d3dd4817b7ae8eda85c5caeb2c10054ae8c6388c2a85e84e76128 1f20fa3a54cf91280b9ea9cc3cca4ba2913b00922a0c924383303c2018f7643b8cf1ac a1d3cda643ba7bfa01dea08de1c6490708988ce532a6eac9a6b407e977ef9684fd784b dabc0d7ceaeee66634c677dfd37daff740f0f5f400805fe6cbbb0a8e5efe2d2b57d357 da378ccaa3da7a7aaa1b01d3b91871c70edec875994a64a7affaeedc5169b523eab592 e87be0c690aac8517011fef4e95d36a7a0edd57c492a0e908c86193500a4a92ccc67a6 3bb666f42656cdb423cd72b7197a59d3e40e80a421a051e5fcb8cb5fa94ae535ee118d 68b67b3c4b3d81be199e290d6e9e13c2b1ee768972d1fa3731f2371ac8194509753c9b 38ae0010034200286c3c993216657374ac1dda3aae0010a3430028ecb7eea20134048c 675b739f0364694cbb4c88b2d43d6b1ee60068803cc7f32c7519770d000360045a60bc 9991858159f3b40be8db4203a00a7f4fc20734eda0fea947ae00d0a4462e053463a40a d3e66b8676bba641e3a5696f9ab469da9c2b00d2141c4f3b5a0d8c9793f539d45079d4 350f1a59db12e66b1b0084956ad62fa124315371d00ae3c3f0a472e269390780ba4e0a a71717dd141a00a150e282a815572b3c9e5f4041c88080af7e38d0c23784d870d2da2c cd7819edf05c3800a8477ef6d967d1ca952b1d0f399ec54719c4210c2c71efbdf75e74 eeb9e7dab980b35cba0f3ffc305ab56a953bd4e15fc3fa372cf4f0eddbb747279d7492 3be7c7092604ccb9bfe79f7f3e7afbedb7dd27e31c07e3bbc1dededee8f4d34f770742 290bb01415048503807acd962d5ba2b7de7acb3d9e77de79ce0d05b176ed5a773c8c2f 756538c5fbe69b6fba2f7ff9d894a51879246c048be1990f48efbefbee68ddba75ee1b 423ef8dc6fbffda2afbffeda7d44ba66cd9a087bd34d37b9f8b06c955704b7b0004048 1ccfc2e0c784bd1001f349383b6e32089c1e4cd8adb7de1a1d77dc71237fe9820089c7 a00deeb9e71ef711e9bc79f3a29b6fbed9a595909f78e289e8e9a79f761faa5287ebae bb4e4514ce1df78da0ac1c4318a87c2cfeb8517c18a730f20010810821021676e7302f bcf042f9c0664f74c9259738e1332c901f7bc51557b830f6f3f9dc9c9b47001f748b66 0a0b80d160b43667de79e71d37f347e5738f008638ac84cc1c00a1736105430126049b 0b28c04f618780acbc4548088e0b2166cd9ae50448185a60eedcb96ee2c75c81309eb9 2f00a3e1452059ba74a95b0df055d2860d1baad2b88782fcfc2e0180aabfefbefbdcb2 8e719fd93e2b053e30a147b3dcc3688e21d0843245fd6b7e411e8c4012a66b77ffef0e 000889f1fc0cbbc8811ece72908f340e3ae820272b84aac9201f9ed632e41150000326 0928b5f2b74b7861018020b18cc908346e100626de2b058073ce39c7dd39447ea975d2 73850c973c724d1c960d20268812ae5cd43e1f8860b8150ca332dd43417e0a3b096439 474f4580080aa3cd1d04c13e0182951a8fcb43a0c1c542078b39f6d8639d9fb9802678 4a2721bff6da6b2e0dc3c9f2e5cb5dbe38d85c609bff140e0062f292254b4626685ceb c62e1dea1ba17ff0c1076e0d8f6aee2d6fee2007f2caaad7b314c4f22cda5c433367ce 1c07a8279f7cd26dfe0036e249fbe28b2fba0d258681134e38c1dd5a023044b3cd655e 55bdc20d0108819eca7efc65975de626736cf1de76db6d6e6dce260e1f88a21518e7e9 cdf45e0487ab099b7a7bc80dd16608e0ee017602f980934fd0d926269c0f58d96246db 409b8b2a8a6c0a0700984d4fa3c79d79e6996e678f5d3936633efdf453276876efaebc f24a77954c281cc67234029a828da02423da7cfa0d009e7aeaa9e8fdf7df77f7132274 6e13e3b632f6072eb8e0024787ba487b24d16ce7b0ac07f0c9876508996c4c9b6e3d6a 8a5de478d6c71f7ffc88853103cb4adbb23667c4787a33f7f4b0264755b3be474328be 396ad5a9a08950b1ac06366ddae466fdcc29d820a21ce863c642f8569f61e3738769a0 9fec2ea23fd9f37a2b779bd561bb55a1df2c13983db7442db09e6955038cba90eb551e c64b50081d2ba3703da775d104d040c8f47add23283a0c27a4190be1abcc3a6e6639b4 0a803a751a9b2884804150618f54782bb5108d386d84ce9ce2b7605a0540a872f087cf 63ca1f84325abd713469676492782d7ecb4d4dae5500a8402a30643d6508d5886bcf99 d592884eb8150ed870346c93d72ee32fbb5e99c6fb0ab58aaf15000875b8d892f594a1 b26a442f4f00a0c2e7967d000022c6df4ee33320f0b760b4a875b30280ca20605c7a3e ffe5d26dffb4f1dd35d75cb3c29ed91ce7240603e504188c092d18f178c0561e43b6da f9dec0c0ccbf647c160832936fa5979217e1022204cef11c2c0bec1966b9f189f00910 1813321a277ccbcbebc9dd66fbcc6e0bfcbcad225c40207d2a935503a81055100db0db 5453b7a92868f27e94718aca0100014dae054d98061c9030e123020604fdf47a33f097 b5bf046fde6ca6158128afb400e7a950fbf47a2c40c0924e69cd3b61527220ec640e04 969f8e85a5e30182cc93c25634001543b0b8540223bf7afec4f8eff9d2ea2f02962680 d7085ec287e7994dab3d53f971b1085c2a1f3f4669fcd3c46f160e2064598141bd5ee1 59e8e6221c093874437fa68a4d64da8303a1a0e3fe3d12371b2041359bbe5eba38adf8 73bdbc1371cd7100c18726fe1cc635e5ff7f6d102cb21055ee1c0000000049454e44ae 426082 """.strip().replace("\n", "") testImageData = b"\x89PNG\r\n\x1a\n" + testPNGData.encode('utf-8') class TestImage(unittest.TestCase): def getImage_generic(self): image, _ = self.objectGenerator("image") image.data = testImageData image.transformation = (1, 0, 0, 1, 0, 0) image.color = (1, 0, 1, 1) return image # ---- # repr # ---- def test_reprContents(self): image = self.getImage_generic() value = image._reprContents() self.assertIsInstance(value, list) color = False glyph = False for i in value: self.assertIsInstance(i, str) if "color" in i: color = True if "in glyph" in i: glyph = True self.assertTrue(color) self.assertFalse(glyph) def test_reprContents_noColor(self): image, _ = self.objectGenerator("image") image.data = testImageData value = image._reprContents() self.assertIsInstance(value, list) color = False glyph = False for i in value: self.assertIsInstance(i, str) if "color" in i: color = True if "in glyph" in i: glyph = True self.assertFalse(color) self.assertFalse(glyph) def test_reprContents_glyph(self): glyph, _ = self.objectGenerator("glyph") image = glyph.image value = image._reprContents() self.assertIsInstance(value, list) color = False glyph = False for i in value: self.assertIsInstance(i, str) if "color=" in value: color = i if "in glyph" in i: glyph = True self.assertFalse(color) self.assertTrue(glyph) def test_reprContents_glyph_color(self): glyph, _ = self.objectGenerator("glyph") image = glyph.image image.color = (1, 0, 1, 1) value = image._reprContents() self.assertIsInstance(value, list) color = False glyph = False for i in value: self.assertIsInstance(i, str) if "color=" in i: color = True if "in glyph" in i: glyph = True self.assertTrue(color) self.assertTrue(glyph) # ---- # bool # ---- def test_bool_data(self): image = self.getImage_generic() self.assertTrue(image) def test_bool_no_data(self): image, _ = self.objectGenerator("image") self.assertFalse(image) def test_bool_data_len_zero(self): image, _ = self.objectGenerator("image") try: image.data = "".encode('utf-8') except FontPartsError: raise unittest.SkipTest("Cannot set zero data") self.assertFalse(image) # ------- # Parents # ------- def test_get_parent_font(self): font, _ = self.objectGenerator("font") layer = font.newLayer("L") glyph = layer.newGlyph("X") image = glyph.image self.assertIsNotNone(image.font) self.assertEqual( image.font, font ) def test_get_parent_noFont(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") image = glyph.image self.assertIsNone(image.font) def test_get_parent_layer(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") image = glyph.image self.assertIsNotNone(image.layer) self.assertEqual( image.layer, layer ) def test_get_parent_noLayer(self): glyph, _ = self.objectGenerator("glyph") image = glyph.image self.assertIsNone(image.font) self.assertIsNone(image.layer) def test_get_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") image = glyph.image self.assertIsNotNone(image.glyph) self.assertEqual( image.glyph, glyph ) def test_get_parent_noGlyph(self): image, _ = self.objectGenerator("image") self.assertIsNone(image.font) self.assertIsNone(image.layer) self.assertIsNone(image.glyph) def test_set_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") image = self.getImage_generic() image.glyph = glyph self.assertIsNotNone(image.glyph) self.assertEqual( image.glyph, glyph ) def test_set_parent_glyph_none(self): image, _ = self.objectGenerator("image") image.glyph = None self.assertIsNone(image.glyph) def test_set_parent_glyph_exists(self): glyph, _ = self.objectGenerator("glyph") otherGlyph, _ = self.objectGenerator("glyph") image = glyph.image with self.assertRaises(AssertionError): image.glyph = otherGlyph # ---- # Data # ---- def test_data_get(self): image = self.getImage_generic() # get self.assertEqual( image.data, testImageData ) def test_data_set_valid(self): image = self.getImage_generic() image.data = testImageData self.assertEqual( image.data, testImageData ) def test_data_get_direct(self): image = self.getImage_generic() # get self.assertEqual( image._get_base_data(), testImageData ) def test_data_set_valid_direct(self): image = self.getImage_generic() image._set_base_data(testImageData) self.assertEqual( image.data, testImageData ) def test_data_set_invalid(self): image = self.getImage_generic() with self.assertRaises(FontPartsError): image.data = 123 def test_data_set_invalid_png(self): image, _ = self.objectGenerator("image") with self.assertRaises(FontPartsError): image.data = testPNGData.encode('utf-8') # ----- # Color # ----- def test_get_color_no_parent(self): image = self.getImage_generic() self.assertEqual( image.color, (1, 0, 1, 1) ) def test_get_color_parent(self): font, _ = self.objectGenerator("font") layer = font.layers[0] glyph = layer.newGlyph("A") image = glyph.image image.data = testImageData image.transformation = (1, 0, 0, 1, 0, 0) image.color = (1, 0, 1, 1) self.assertEqual( image.color, (1, 0, 1, 1) ) def test_get_color_no_parent_none(self): image = self.getImage_generic() image.color = None self.assertEqual( image.color, None ) def test_get_color_parent_none(self): font, _ = self.objectGenerator("font") layer = font.layers[0] glyph = layer.newGlyph("A") image = glyph.image image.data = testImageData image.transformation = (1, 0, 0, 1, 0, 0) self.assertEqual( image.color, None ) def test_set_color(self): image = self.getImage_generic() image.color = (0, 1, 0, 0) self.assertEqual( image.color, (0, 1, 0, 0) ) image.color = (0.5, 0.5, 0.5, 0.5) self.assertEqual( image.color, (0.5, 0.5, 0.5, 0.5) ) def test_set_color_invalid(self): image = self.getImage_generic() with self.assertRaises(ValueError): image.color = (0, 4, 0, 0) # -------------- # Transformation # -------------- def test_get_transformation(self): image = self.getImage_generic() self.assertEqual( image.transformation, (1, 0, 0, 1, 0, 0) ) def test_set_tranformation(self): image = self.getImage_generic() image.transformation = (0, 1, 1, 0, 1, 1) self.assertEqual( image.transformation, (0, 1, 1, 0, 1, 1) ) image.transformation = (0.5, 0.5, 0.5, 0.5, 0.5, 0.5) self.assertEqual( image.transformation, (0.5, 0.5, 0.5, 0.5, 0.5, 0.5) ) def test_set_tranformation_invalid(self): image = self.getImage_generic() with self.assertRaises(TypeError): image.transformation = (0, 1, "a", 0, 1, 1) def test_transformBy_valid_no_origin(self): image = self.getImage_generic() image.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual( image.transformation, (2, 0, 0, 3, -3, 2) ) def test_transformBy_valid_origin(self): image = self.getImage_generic() image.transformBy((2, 0, 0, 2, 0, 0), origin=(1, 2)) self.assertEqual( image.transformation, (2, 0, 0, 2, -1, -2) ) # ------ # Offset # ------ def test_get_offset(self): image = self.getImage_generic() self.assertEqual( image.offset, (0, 0) ) def test_get_offset_set(self): image = self.getImage_generic() image.offset = (1, 4.5) self.assertEqual( image.offset, (1, 4.5) ) def test_set_offset(self): image = self.getImage_generic() image.offset = (2.3, 5) self.assertEqual( image.offset, (2.3, 5) ) def test_set_offset_invalid_none(self): image = self.getImage_generic() with self.assertRaises(TypeError): image.offset = None def test_set_offset_invalid_string(self): image = self.getImage_generic() with self.assertRaises(TypeError): image.offset = ("a", "b") # ----- # Scale # ----- def test_get_scale(self): image = self.getImage_generic() self.assertEqual( image.scale, (1, 1) ) def test_get_scale_set(self): image = self.getImage_generic() image.scale = (2, 2.5) self.assertEqual( image.scale, (2, 2.5) ) def test_set_scale(self): image = self.getImage_generic() image.scale = (2.3, 5) self.assertEqual( image.scale, (2.3, 5) ) def test_set_scale_invalid_none(self): image = self.getImage_generic() with self.assertRaises(TypeError): image.scale = None def test_set_scale_invalid_string(self): image = self.getImage_generic() with self.assertRaises(TypeError): image.scale = ("a", "b") # ------------- # Normalization # ------------- def test_round(self): image = self.getImage_generic() image.offset = (1.1, 1.1) image.round() self.assertEqual( image.offset, (1, 1) ) def test_round_half(self): image = self.getImage_generic() image.offset = (1.5, 1.5) image.round() self.assertEqual( image.offset, (2, 2) ) # ---- # Hash # ---- def test_hash_object_self(self): image_one = self.getImage_generic() self.assertEqual( hash(image_one), hash(image_one) ) def test_hash_object_other(self): image_one = self.getImage_generic() image_two = self.getImage_generic() self.assertNotEqual( hash(image_one), hash(image_two) ) def test_hash_object_self_variable_assignment(self): image_one = self.getImage_generic() a = image_one self.assertEqual( hash(image_one), hash(a) ) def test_hash_object_other_variable_assignment(self): image_one = self.getImage_generic() image_two = self.getImage_generic() a = image_one self.assertNotEqual( hash(image_two), hash(a) ) def test_is_hashable(self): image_one = self.getImage_generic() self.assertTrue( isinstance(image_one, collections.abc.Hashable) ) # -------- # Equality # -------- def test_object_equal_self(self): image_one = self.getImage_generic() self.assertEqual( image_one, image_one ) def test_object_not_equal_other(self): image_one = self.getImage_generic() image_two = self.getImage_generic() self.assertNotEqual( image_one, image_two ) def test_object_equal_self_variable_assignment(self): image_one = self.getImage_generic() a = image_one self.assertEqual( image_one, a ) def test_object_not_equal_other_variable_assignment(self): image_one = self.getImage_generic() image_two = self.getImage_generic() a = image_one self.assertNotEqual( image_two, a ) # --------- # Selection # --------- def test_selected_true(self): image = self.getImage_generic() try: image.selected = False except NotImplementedError: return image.selected = True self.assertEqual( image.selected, True ) def test_selected_false(self): image = self.getImage_generic() try: image.selected = False except NotImplementedError: return self.assertEqual( image.selected, False ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_info.py000066400000000000000000000103231477533125200241560ustar00rootroot00000000000000import unittest import collections class TestInfo(unittest.TestCase): def getInfo_generic(self): info, _ = self.objectGenerator("info") info.unitsPerEm = 1000 return info # ---------- # Dimensions # ---------- def test_get_unitsPerEm(self): info = self.getInfo_generic() self.assertEqual( info.unitsPerEm, 1000 ) def test_set_valid_unitsPerEm_int(self): info = self.getInfo_generic() info.unitsPerEm = 2000 self.assertEqual( info.unitsPerEm, 2000 ) def test_set_valid_unitsPerEm_float(self): info = self.getInfo_generic() info.unitsPerEm = 2000.1 self.assertEqual( info.unitsPerEm, 2000.1 ) def test_set_invalid_unitsPerEm_negative(self): info = self.getInfo_generic() with self.assertRaises(ValueError): info.unitsPerEm = -1000 def test_set_invalid_unitsPerEm_string(self): info = self.getInfo_generic() with self.assertRaises(ValueError): info.unitsPerEm = "abc" # ---- # Hash # ---- def test_hash(self): info = self.getInfo_generic() self.assertEqual( isinstance(info, collections.abc.Hashable), True ) # -------- # Equality # -------- def test_object_equal_self(self): info_one = self.getInfo_generic() self.assertEqual( info_one, info_one ) def test_object_not_equal_other(self): info_one = self.getInfo_generic() info_two = self.getInfo_generic() self.assertNotEqual( info_one, info_two ) def test_object_equal_self_variable_assignment(self): info_one = self.getInfo_generic() a = info_one self.assertEqual( info_one, a ) def test_object_not_equal_other_variable_assignment(self): info_one = self.getInfo_generic() info_two = self.getInfo_generic() a = info_one self.assertNotEqual( info_two, a ) # ----- # Round # ----- def test_round_unitsPerEm(self): info = self.getInfo_generic() info.unitsPerEm = 2000.125 info.round() self.assertEqual( info.unitsPerEm, 2000 ) # ------ # Update # ------ def test_update(self): from fontTools.ufoLib import fontInfoAttributesVersion3ValueData info1 = self.getInfo_generic() info1.familyName = "test1" info1.unitsPerEm = 1000 info2 = self.getInfo_generic() info2.familyName = "test2" info2.unitsPerEm = 2000 info1.update(info2) self.assertEqual(info1.familyName, "test2") self.assertEqual(info1.unitsPerEm, 2000) # ---- # Copy # ---- def test_copy(self): info1 = self.getInfo_generic() info1.postscriptBlueValues = [-10, 0, 50, 60] info2 = info1.copy() info2.postscriptBlueValues[0] = -2 self.assertNotEqual(info1.postscriptBlueValues, info2.postscriptBlueValues) # ------------- # Interpolation # ------------- def test_interpolate_unitsPerEm_without_rounding(self): interpolated_font, _ = self.objectGenerator("font") font_min, _ = self.objectGenerator("font") font_max, _ = self.objectGenerator("font") font_min.info.unitsPerEm = 1000 font_max.info.unitsPerEm = 2000 interpolated_font.info.interpolate(0.5154, font_min.info, font_max.info, round=False) self.assertEqual( interpolated_font.info.unitsPerEm, 1515.4 ) def test_interpolate_unitsPerEm_with_rounding(self): interpolated_font, _ = self.objectGenerator("font") font_min, _ = self.objectGenerator("font") font_max, _ = self.objectGenerator("font") font_min.info.unitsPerEm = 1000 font_max.info.unitsPerEm = 2000 interpolated_font.info.interpolate(0.5154, font_min.info, font_max.info, round=True) self.assertEqual( interpolated_font.info.unitsPerEm, 1515 ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_kerning.py000066400000000000000000000165641477533125200246750ustar00rootroot00000000000000import unittest import collections class TestKerning(unittest.TestCase): def getKerning_generic(self): font, _ = self.objectGenerator("font") groups = font.groups groups["public.kern1.X"] = ["A", "B", "C"] groups["public.kern2.X"] = ["A", "B", "C"] kerning = font.kerning kerning.update({ ("public.kern1.X", "public.kern2.X"): 100, ("B", "public.kern2.X"): 101, ("public.kern1.X", "B"): 102, ("A", "A"): 103, }) return kerning def getKerning_font2(self): font, _ = self.objectGenerator("font") groups = font.groups groups["public.kern1.X"] = ["A", "B", "C"] groups["public.kern2.X"] = ["A", "B", "C"] kerning = font.kerning kerning.update({ ("public.kern1.X", "public.kern2.X"): 200, ("B", "public.kern2.X"): 201, ("public.kern1.X", "B"): 202, ("A", "A"): 203, }) return kerning # --- # len # --- def test_len_initial(self): kerning = self.getKerning_generic() self.assertEqual( len(kerning), 4 ) def test_len_clear(self): kerning = self.getKerning_generic() kerning.clear() self.assertEqual( len(kerning), 0 ) # -------- # contains # -------- def test_contains_glyph_glyph(self): kerning = self.getKerning_generic() self.assertEqual( ('A', 'A') in kerning, True ) def test_contains_group_group(self): kerning = self.getKerning_generic() self.assertEqual( ("public.kern1.X", "public.kern2.X") in kerning, True ) def test_contains_glyph_group(self): kerning = self.getKerning_generic() self.assertEqual( ("B", "public.kern2.X") in kerning, True ) def test_contains_missing_glyph_glyph(self): kerning = self.getKerning_generic() self.assertEqual( ("H", "H") in kerning, False ) # --- # del # --- def test_del(self): kerning = self.getKerning_generic() # Be sure it is here before deleting self.assertEqual( ('A', 'A') in kerning, True ) # Delete del kerning[('A', 'A')] # Test self.assertEqual( ('A', 'A') in kerning, False ) # --- # get # --- def test_get_glyph_glyph(self): kerning = self.getKerning_generic() self.assertEqual( kerning[('A', 'A')], 103 ) def test_get_group_group(self): kerning = self.getKerning_generic() self.assertEqual( kerning[("public.kern1.X", "public.kern2.X")], 100 ) def test_get_glyph_group(self): kerning = self.getKerning_generic() self.assertEqual( kerning[("B", "public.kern2.X")], 101 ) def test_get_group_glyph(self): kerning = self.getKerning_generic() self.assertEqual( kerning[("public.kern1.X", "B")], 102 ) def test_get_fallback_default(self): kerning = self.getKerning_generic() self.assertEqual( kerning.get(("F", "F")), None ) def test_get_fallback_default_user(self): kerning = self.getKerning_generic() self.assertEqual( kerning.get(("F", "F"), None), None ) self.assertEqual( kerning.get(("F", "F"), 0), 0 ) # --- # set # --- def test_set_glyph_glyph(self): kerning = self.getKerning_generic() kerning[('A', 'A')] = 1 self.assertEqual( kerning[('A', 'A')], 1 ) def test_set_group_group(self): kerning = self.getKerning_generic() kerning[("public.kern1.X", "public.kern2.X")] = 2 self.assertEqual( kerning[("public.kern1.X", "public.kern2.X")], 2 ) def test_set_glyph_group(self): kerning = self.getKerning_generic() kerning[("B", "public.kern2.X")] = 3 self.assertEqual( kerning[("B", "public.kern2.X")], 3 ) def test_set_group_glyph(self): kerning = self.getKerning_generic() kerning[("public.kern1.X", "B")] = 4 self.assertEqual( kerning[("public.kern1.X", "B")], 4 ) # ---- # Find # ---- def test_find_glyph_glyph(self): kerning = self.getKerning_generic() self.assertEqual( kerning.find(('A', 'A')), 103 ) def test_find_glyph_glyph_none(self): kerning = self.getKerning_generic() self.assertEqual( kerning.find(('D', 'D')), None ) def test_find_group_glyph(self): kerning = self.getKerning_generic() self.assertEqual( kerning.find(('A', 'B')), 102 ) def test_find_glyph_group(self): kerning = self.getKerning_generic() self.assertEqual( kerning.find(('B', 'B')), 101 ) def test_find_group_group(self): kerning = self.getKerning_generic() self.assertEqual( kerning.find(('C', 'C')), 100 ) # ---- # Hash # ---- def test_hash(self): kerning = self.getKerning_generic() self.assertEqual( isinstance(kerning, collections.abc.Hashable), True ) # -------- # Equality # -------- def test_object_equal_self(self): kerning_one = self.getKerning_generic() self.assertEqual( kerning_one, kerning_one ) def test_object_not_equal_other(self): kerning_one = self.getKerning_generic() kerning_two = self.getKerning_generic() self.assertNotEqual( kerning_one, kerning_two ) def test_object_equal_self_variable_assignment(self): kerning_one = self.getKerning_generic() a = kerning_one self.assertEqual( kerning_one, a ) def test_object_not_equal_other_variable_assignment(self): kerning_one = self.getKerning_generic() kerning_two = self.getKerning_generic() a = kerning_one self.assertNotEqual( kerning_two, a ) # ------------- # Interpolation # ------------- def test_interpolation_without_rounding(self): interpolated = self.getKerning_generic() kerning_min = self.getKerning_generic() kerning_max = self.getKerning_font2() interpolated.interpolate(0.515, kerning_min, kerning_max, round=False) self.assertEqual( interpolated[("public.kern1.X", "public.kern2.X")], 151.5 ) def test_interpolation_with_rounding(self): interpolated = self.getKerning_generic() kerning_min = self.getKerning_generic() kerning_max = self.getKerning_font2() interpolated.interpolate(0.515, kerning_min, kerning_max, round=True) self.assertEqual( interpolated[("public.kern1.X", "public.kern2.X")], 152 ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_layer.py000066400000000000000000000154671477533125200243550ustar00rootroot00000000000000import unittest import collections class TestLayer(unittest.TestCase): # ------ # Glyphs # ------ def getLayer_glyphs(self): layer, _ = self.objectGenerator("layer") for name in "ABCD": layer.newGlyph(name) return layer def test_len(self): layer = self.getLayer_glyphs() self.assertEqual( len(layer), 4 ) def _testInsertGlyph(self, setGlyphName=True): layer, _ = self.objectGenerator("layer") glyph, _ = self.objectGenerator("glyph") pen = glyph.getPen() pen.moveTo((100, 0)) pen.lineTo((100, 500)) pen.lineTo((500, 500)) pen.closePath() glyph.width = 600 if setGlyphName: glyph.name = "test" layer["test"] = glyph self.assertTrue("test" in layer) self.assertEqual( layer["test"].bounds, glyph.bounds ) def test_set_glyph(self): self._testInsertGlyph(setGlyphName=True) def test_set_glyph_with_name_None(self): self._testInsertGlyph(setGlyphName=False) def test_get_glyph_in_font(self): layer = self.getLayer_glyphs() self.assertEqual( layer["A"].name, "A" ) def test_get_glyph_not_in_font(self): layer = self.getLayer_glyphs() with self.assertRaises(KeyError): layer["E"] # ---- # Hash # ---- def test_hash_object_self(self): layer_one = self.getLayer_glyphs() self.assertEqual( hash(layer_one), hash(layer_one) ) def test_hash_object_other(self): layer_one = self.getLayer_glyphs() layer_two = self.getLayer_glyphs() self.assertNotEqual( hash(layer_one), hash(layer_two) ) def test_hash_object_self_variable_assignment(self): layer_one = self.getLayer_glyphs() a = layer_one self.assertEqual( hash(layer_one), hash(a) ) def test_hash_object_other_variable_assignment(self): layer_one = self.getLayer_glyphs() layer_two = self.getLayer_glyphs() a = layer_one self.assertNotEqual( hash(layer_two), hash(a) ) def test_is_hashable(self): layer_one = self.getLayer_glyphs() self.assertTrue( isinstance(layer_one, collections.abc.Hashable) ) # -------- # Equality # -------- def test_object_equal_self(self): layer_one = self.getLayer_glyphs() self.assertEqual( layer_one, layer_one ) def test_object_not_equal_other(self): layer_one = self.getLayer_glyphs() layer_two = self.getLayer_glyphs() self.assertNotEqual( layer_one, layer_two ) def test_object_equal_self_variable_assignment(self): layer_one = self.getLayer_glyphs() a = layer_one self.assertEqual( layer_one, a ) def test_object_not_equal_self_variable_assignment(self): layer_one = self.getLayer_glyphs() layer_two = self.getLayer_glyphs() a = layer_one self.assertNotEqual( layer_two, a ) # --------- # Selection # --------- def test_selected_true(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return layer.selected = True self.assertEqual( layer.selected, True ) def test_selected_false(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return self.assertEqual( layer.selected, False ) # Glyphs def test_selectedGlyphs_default(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return self.assertEqual( layer.selectedGlyphs, () ) def test_selectedGlyphs_setSubObject(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return glyph1 = layer["A"] glyph2 = layer["B"] glyph1.selected = True glyph2.selected = True self.assertEqual( tuple(sorted(layer.selectedGlyphs, key=lambda glyph: glyph.name)), (glyph1, glyph2) ) def test_selectedGlyphs_setFilledList(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return glyph3 = layer["C"] glyph4 = layer["D"] layer.selectedGlyphs = [glyph3, glyph4] self.assertEqual( tuple(sorted(layer.selectedGlyphs, key=lambda glyph: glyph.name)), (glyph3, glyph4) ) def test_selectedGlyphs_setEmptyList(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return glyph1 = layer["A"] glyph1.selected = True layer.selectedGlyphs = [] self.assertEqual( layer.selectedGlyphs, () ) # Glyph Names def test_selectedGlyphNames_default(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return self.assertEqual( layer.selectedGlyphs, () ) def test_selectedGlyphNames_setSubObject(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return glyph1 = layer["A"] glyph2 = layer["B"] glyph1.selected = True glyph2.selected = True self.assertEqual( tuple(sorted(layer.selectedGlyphNames)), ("A", "B") ) def test_selectedGlyphNames_setFilledList(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return layer.selectedGlyphNames = ["C", "D"] self.assertEqual( tuple(sorted(layer.selectedGlyphNames)), ("C", "D") ) def test_selectedGlyphNames_setEmptyList(self): layer = self.getLayer_glyphs() try: layer.selected = False except NotImplementedError: return glyph1 = layer["A"] glyph1.selected = True layer.selectedGlyphNames = [] self.assertEqual( layer.selectedGlyphNames, () ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_lib.py000066400000000000000000000075241477533125200240020ustar00rootroot00000000000000import unittest import collections class TestLib(unittest.TestCase): def getLib_generic(self): lib, _ = self.objectGenerator("lib") lib.update({ "key 1": ["A", "B", "C"], "key 2": "x", "key 3": [], "key 4": 20 }) return lib # ---- # repr # ---- def test_reprContents(self): lib = self.getLib_generic() value = lib._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) # --- # len # --- def test_len_initial(self): lib = self.getLib_generic() self.assertEqual( len(lib), 4 ) def test_len_clear(self): lib = self.getLib_generic() lib.clear() self.assertEqual( len(lib), 0 ) # ------- # Parents # ------- # Glyph def test_get_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") lib, _ = self.objectGenerator("lib") lib.glyph = glyph self.assertIsNotNone(lib.glyph) self.assertEqual( lib.glyph, glyph ) def test_get_parent_noGlyph(self): lib, _ = self.objectGenerator("lib") self.assertIsNone(lib.font) self.assertIsNone(lib.layer) self.assertIsNone(lib.glyph) def test_set_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") lib, _ = self.objectGenerator("lib") lib.glyph = glyph self.assertIsNotNone(lib.glyph) self.assertEqual( lib.glyph, glyph ) # Font def test_get_parent_font(self): font, _ = self.objectGenerator("font") layer = font.newLayer("L") glyph = layer.newGlyph("X") lib, _ = self.objectGenerator("lib") lib.glyph = glyph self.assertIsNotNone(lib.font) self.assertEqual( lib.font, font ) def test_get_parent_noFont(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") lib, _ = self.objectGenerator("lib") lib.glyph = glyph self.assertIsNone(lib.font) # ------- # Queries # ------- def test_keys(self): lib = self.getLib_generic() self.assertEqual( sorted(lib.keys()), ["key 1", "key 2", "key 3", "key 4"] ) def test_contains_found(self): lib = self.getLib_generic() self.assertTrue("key 4" in lib) def test_contains_not_found(self): lib = self.getLib_generic() self.assertFalse("key five" in lib) def test_get_found(self): lib = self.getLib_generic() self.assertEqual( lib["key 1"], ["A", "B", "C"] ) # ---- # Hash # ---- def test_hash(self): lib = self.getLib_generic() self.assertEqual( isinstance(lib, collections.abc.Hashable), True ) # -------- # Equality # -------- def test_object_equal_self(self): lib_one = self.getLib_generic() self.assertEqual( lib_one, lib_one ) def test_object_not_equal_other(self): lib_one = self.getLib_generic() lib_two = self.getLib_generic() self.assertNotEqual( lib_one, lib_two ) def test_object_equal_self_variable_assignment(self): lib_one = self.getLib_generic() a = lib_one self.assertEqual( lib_one, a ) def test_object_not_equal_other_variable_assignment(self): lib_one = self.getLib_generic() lib_two = self.getLib_generic() a = lib_one self.assertNotEqual( lib_two, a ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_normalizers.py000066400000000000000000002146201477533125200255760ustar00rootroot00000000000000import unittest from fontParts.base import normalizers class TestNormalizers(unittest.TestCase): # ---- # Font # ---- def getFont_layers(self): font, _ = self.objectGenerator("font") for name in ["A", "B", "C", "D", "E"]: font.newLayer(name) return font # normalizeFileFormatVersion def test_normalizeFileFormatVersion_int(self): result = normalizers.normalizeFileFormatVersion(3) self.assertIsInstance(result, int) self.assertEqual(result, 3) def test_normalizeFileFormatVersion_float(self): with self.assertRaises(TypeError): normalizers.normalizeFileFormatVersion(3.0) def test_normalizeFileFormatVersion_invalid(self): with self.assertRaises(TypeError): normalizers.normalizeFileFormatVersion("3") # normalizeLayerOrder def test_normalizeLayerOrder_valid(self): font = self.getFont_layers() result = normalizers.normalizeLayerOrder(["A", "B", "C", "D", "E"], font) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, str) self.assertEqual(result, (u"A", u"B", u"C", u"D", u"E")) def test_normalizeLayerOrder_validTuple(self): font = self.getFont_layers() result = normalizers.normalizeLayerOrder(tuple(["A", "B", "C", "D", "E"]), font) self.assertEqual(result, (u"A", u"B", u"C", u"D", u"E")) def test_normalizeLayerOrder_notList(self): font = self.getFont_layers() with self.assertRaises(TypeError): normalizers.normalizeLayerOrder("A B C D E", font) def test_normalizeLayerOrder_invalidMember(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphOrder(["A", "B", "C", "D", 2]) def test_normalizeLayerOrder_notInFont(self): font = self.getFont_layers() with self.assertRaises(ValueError): normalizers.normalizeLayerOrder(["A", "B", "C", "D", "E", "X"], font) def test_normalizeLayerOrder_duplicate(self): font = self.getFont_layers() with self.assertRaises(ValueError): normalizers.normalizeLayerOrder(["A", "B", "C", "C", "D", "E"], font) # normalizeDefaultLayerName def test_normalizeDefaultLayerName_valid(self): font = self.getFont_layers() result = normalizers.normalizeDefaultLayerName("B", font) self.assertIsInstance(result, str) self.assertEqual(result, u"B") def test_normalizeDefaultLayerName_notValidLayerName(self): font = self.getFont_layers() with self.assertRaises(TypeError): normalizers.normalizeDefaultLayerName(1, font) def test_normalizeDefaultLayerName_notInFont(self): font = self.getFont_layers() with self.assertRaises(ValueError): normalizers.normalizeDefaultLayerName("X", font) # normalizeGlyphOrder def test_normalizeGlyphOrder_valid(self): result = normalizers.normalizeGlyphOrder(["A", "B", "C", "D", "E"]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, str) self.assertEqual(result, (u"A", u"B", u"C", u"D", u"E")) def test_normalizeGlyphOrder_validTuple(self): result = normalizers.normalizeGlyphOrder(tuple(["A", "B", "C", "D", "E"])) self.assertEqual(result, (u"A", u"B", u"C", u"D", u"E")) def test_normalizeGlyphOrder_notList(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphOrder("A B C D E") def test_normalizeGlyphOrder_invalidMember(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphOrder(["A", "B", "C", "D", 2]) def test_normalizeGlyphOrder_duplicate(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphOrder(["A", "B", "C", "C", "D", "E"]) # ------- # Kerning # ------- # normalizeKerningKey def test_normalizeKerningKey_validGlyphs(self): result = normalizers.normalizeKerningKey(("A", "B")) self.assertIsInstance(result, tuple) self.assertEqual(result, (u"A", u"B")) def test_normalizeKerningKey_validGroups(self): result = normalizers.normalizeKerningKey(("public.kern1.A", "public.kern2.B")) self.assertIsInstance(result, tuple) self.assertEqual(result, (u"public.kern1.A", u"public.kern2.B")) def test_normalizeKerningKey_validList(self): result = normalizers.normalizeKerningKey(["A", "B"]) self.assertEqual(result, (u"A", u"B")) def test_normalizeKerningKey_notTuple(self): with self.assertRaises(TypeError): normalizers.normalizeKerningKey("A B") def test_normalizeKerningKey_notEnoughMembers(self): with self.assertRaises(ValueError): normalizers.normalizeKerningKey(("A",)) def test_normalizeKerningKey_tooManyMembers(self): with self.assertRaises(ValueError): normalizers.normalizeKerningKey(("A", "B", "C")) def test_normalizeKerningKey_memberNotString(self): with self.assertRaises(TypeError): normalizers.normalizeKerningKey(("A", 2)) with self.assertRaises(TypeError): normalizers.normalizeKerningKey((1, "B")) def test_normalizeKerningKey_memberNotLongEnough(self): with self.assertRaises(ValueError): normalizers.normalizeKerningKey(("A", "")) with self.assertRaises(ValueError): normalizers.normalizeKerningKey(("", "B")) def test_normalizeKerningKey_invalidSide1Group(self): with self.assertRaises(ValueError): normalizers.normalizeKerningKey(("public.kern2.A", "B")) def test_normalizeKerningKey_invalidSide2Group(self): with self.assertRaises(ValueError): normalizers.normalizeKerningKey(("A", "public.kern1.B")) # normalizeKerningValue def test_normalizeKerningValue_zero(self): result = normalizers.normalizeKerningValue(0) self.assertIsInstance(result, int) self.assertEqual(result, 0) def test_normalizeKerningValue_positiveInt(self): result = normalizers.normalizeKerningValue(1) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeKerningValue_negativeInt(self): result = normalizers.normalizeKerningValue(-1) self.assertIsInstance(result, int) self.assertEqual(result, -1) def test_normalizeKerningValue_positiveFloat(self): result = normalizers.normalizeKerningValue(1.0) self.assertIsInstance(result, float) self.assertEqual(result, 1.0) def test_normalizeKerningValue_negativeFloat(self): result = normalizers.normalizeKerningValue(-1.0) self.assertIsInstance(result, float) self.assertEqual(result, -1.0) def test_normalizeKerningValue_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeKerningValue("1") # ------ # Groups # ------ # normalizeGroupKey def test_normalizeGroupKey_valid(self): result = normalizers.normalizeGroupKey("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizeGroupKey_notString(self): with self.assertRaises(TypeError): normalizers.normalizeGroupKey(1) def test_normalizeGroupKey_notLongEnough(self): with self.assertRaises(ValueError): normalizers.normalizeGroupKey("") # normalizeGroupValue def test_normalizeGroupValue_valid(self): result = normalizers.normalizeGroupValue(["A", "B", "C"]) self.assertIsInstance(result, tuple) self.assertEqual(result, (u"A", u"B", u"C")) def test_normalizeGroupValue_validTuple(self): result = normalizers.normalizeGroupValue(("A", "B", "C")) self.assertEqual(result, (u"A", u"B", u"C")) def test_normalizeGroupValue_validEmpty(self): result = normalizers.normalizeGroupValue([]) self.assertEqual(result, tuple()) def test_normalizeGroupValue_notList(self): with self.assertRaises(TypeError): normalizers.normalizeGroupValue("A B C") def test_normalizeGroupValue_invalidMember(self): with self.assertRaises(TypeError): normalizers.normalizeGroupValue(["A", "B", 3]) # -------- # Features # -------- # normalizeFeatureText def test_normalizeFeatureText_valid(self): result = normalizers.normalizeFeatureText("test") self.assertIsInstance(result, str) self.assertEqual(result, u"test") def test_normalizeFeatureText_notString(self): with self.assertRaises(TypeError): normalizers.normalizeFeatureText(123) # --- # Lib # --- # normalizeLibKey def test_normalizeLibKey_valid(self): result = normalizers.normalizeLibKey("test") self.assertIsInstance(result, str) self.assertEqual(result, u"test") def test_normalizeLibKey_notString(self): with self.assertRaises(TypeError): normalizers.normalizeLibKey(123) def test_normalizeLibKey_emptyString(self): with self.assertRaises(ValueError): normalizers.normalizeLibKey("") # normalizeLibValue def test_normalizeLibValue_invalidNone(self): with self.assertRaises(ValueError): normalizers.normalizeLibValue(None) def test_normalizeLibValue_validString(self): result = normalizers.normalizeLibValue("test") self.assertIsInstance(result, str) self.assertEqual(result, u"test") def test_normalizeLibValue_validInt(self): result = normalizers.normalizeLibValue(1) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeLibValue_validFloat(self): result = normalizers.normalizeLibValue(1.0) self.assertIsInstance(result, float) self.assertEqual(result, 1.0) def test_normalizeLibValue_validTuple(self): result = normalizers.normalizeLibValue(("A", "B")) self.assertIsInstance(result, tuple) self.assertEqual(result, (u"A", u"B")) def test_normalizeLibValue_invalidTupleMember(self): with self.assertRaises(ValueError): normalizers.normalizeLibValue((1, None)) def test_normalizeLibValue_validList(self): result = normalizers.normalizeLibValue(["A", "B"]) self.assertIsInstance(result, list) self.assertEqual(result, [u"A", u"B"]) def test_normalizeLibValue_invalidListMember(self): with self.assertRaises(ValueError): normalizers.normalizeLibValue([1, None]) def test_normalizeLibValue_validDict(self): result = normalizers.normalizeLibValue({"A": 1, "B": 2}) self.assertIsInstance(result, dict) self.assertEqual(result, {u"A": 1, u"B": 2}) def test_normalizeLibValue_invalidDictKey(self): with self.assertRaises(TypeError): normalizers.normalizeLibValue({1: 1, "B": 2}) def test_normalizeLibValue_invalidDictValue(self): with self.assertRaises(ValueError): normalizers.normalizeLibValue({"A": None, "B": 2}) # ----- # Layer # ----- # normalizeLayer def test_normalizeLayer_valid(self): from fontParts.base.layer import BaseLayer layer, _ = self.objectGenerator("layer") result = normalizers.normalizeLayer(layer) self.assertIsInstance(result, BaseLayer) self.assertEqual(result, layer) def test_normalizeLayer_notLayer(self): with self.assertRaises(TypeError): normalizers.normalizeLayer(123) # normalizeLayerName def test_normalizeLayerName_valid(self): result = normalizers.normalizeLayerName("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizeLayerName_notString(self): with self.assertRaises(TypeError): normalizers.normalizeLayerName(123) def test_normalizeLayerName_notLongEnough(self): with self.assertRaises(ValueError): normalizers.normalizeLayerName("") # ----- # Glyph # ----- # normalizeGlyph def test_normalizeGlyph_valid(self): from fontParts.base.glyph import BaseGlyph glyph, _ = self.objectGenerator("glyph") result = normalizers.normalizeGlyph(glyph) self.assertIsInstance(result, BaseGlyph) self.assertEqual(result, glyph) def test_normalizeGlyph_notGlyph(self): with self.assertRaises(TypeError): normalizers.normalizeGlyph(123) # normalizeGlyphName def test_normalizeGlyphName_valid(self): result = normalizers.normalizeGlyphName("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizeGlyphName_notString(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphName(123) def test_normalizeGlyphName_notLongEnough(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphName("") # normalizeGlyphUnicodes def test_normalizeGlyphUnicodes_valid(self): result = normalizers.normalizeGlyphUnicodes([1, 2, 3]) self.assertIsInstance(result, tuple) self.assertEqual(result, (1, 2, 3)) def test_normalizeGlyphUnicodes_validTuple(self): result = normalizers.normalizeGlyphUnicodes(tuple([1, 2, 3])) self.assertEqual(result, (1, 2, 3)) def test_normalizeGlyphUnicodes_notTupleOrList(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphUnicodes("xyz") def test_normalizeGlyphUnicodes_invalidDuplicateMembers(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphUnicodes([1, 2, 3, 2]) def test_normalizeGlyphUnicodes_invalidMember(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphUnicodes([1, 2, "xyz"]) # normalizeGlyphUnicode def test_normalizeGlyphUnicode_validInt(self): result = normalizers.normalizeGlyphUnicode(1) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeGlyphUnicode_validHex(self): result = normalizers.normalizeGlyphUnicode("0001") self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeGlyphUnicode_validRangeMinimum(self): result = normalizers.normalizeGlyphUnicode(0) self.assertEqual(result, 0) def test_normalizeGlyphUnicode_validRangeMaximum(self): result = normalizers.normalizeGlyphUnicode(1114111) self.assertEqual(result, 1114111) def test_normalizeGlyphUnicode_invalidFloat(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphUnicode(1.0) def test_normalizeGlyphUnicode_invalidString(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphUnicode("xyz") def test_normalizeGlyphUnicode_invalidRangeMinimum(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphUnicode(-1) def test_normalizeGlyphUnicode_invalidRangeMaximum(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphUnicode(1114112) # normalizeGlyphBottomMargin def test_normalizeGlyphBottomMargin_zero(self): result = normalizers.normalizeGlyphBottomMargin(0) self.assertEqual(result, 0) def test_normalizeGlyphBottomMargin_positiveInt(self): result = normalizers.normalizeGlyphBottomMargin(1) self.assertEqual(result, 1) def test_normalizeGlyphBottomMargin_negativeInt(self): result = normalizers.normalizeGlyphBottomMargin(-1) self.assertEqual(result, -1) def test_normalizeGlyphBottomMargin_positiveFloat(self): result = normalizers.normalizeGlyphBottomMargin(1.01) self.assertEqual(result, 1.01) def test_normalizeGlyphBottomMargin_negativeFloat(self): result = normalizers.normalizeGlyphBottomMargin(-1.01) self.assertEqual(result, -1.01) def test_normalizeGlyphBottomMargin_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphBottomMargin("1") # normalizeGlyphLeftMargin def test_normalizeGlyphLeftMargin_zero(self): result = normalizers.normalizeGlyphLeftMargin(0) self.assertEqual(result, 0) def test_normalizeGlyphLeftMargin_positiveInt(self): result = normalizers.normalizeGlyphLeftMargin(1) self.assertEqual(result, 1) def test_normalizeGlyphLeftMargin_negativeInt(self): result = normalizers.normalizeGlyphLeftMargin(-1) self.assertEqual(result, -1) def test_normalizeGlyphLeftMargin_positiveFloat(self): result = normalizers.normalizeGlyphLeftMargin(1.01) self.assertEqual(result, 1.01) def test_normalizeGlyphLeftMargin_negativeFloat(self): result = normalizers.normalizeGlyphLeftMargin(-1.01) self.assertEqual(result, -1.01) def test_normalizeGlyphLeftMargin_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphLeftMargin("1") # normalizeGlyphRightMargin def test_normalizeGlyphRightMargin_zero(self): result = normalizers.normalizeGlyphRightMargin(0) self.assertEqual(result, 0) def test_normalizeGlyphRightMargin_positiveInt(self): result = normalizers.normalizeGlyphRightMargin(1) self.assertEqual(result, 1) def test_normalizeGlyphRightMargin_negativeInt(self): result = normalizers.normalizeGlyphRightMargin(-1) self.assertEqual(result, -1) def test_normalizeGlyphRightMargin_positiveFloat(self): result = normalizers.normalizeGlyphRightMargin(1.01) self.assertEqual(result, 1.01) def test_normalizeGlyphRightMargin_negativeFloat(self): result = normalizers.normalizeGlyphRightMargin(-1.01) self.assertEqual(result, -1.01) def test_normalizeGlyphRightMargin_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphRightMargin("1") # normalizeGlyphHeight def test_normalizeGlyphHeight_zero(self): result = normalizers.normalizeGlyphHeight(0) self.assertEqual(result, 0) def test_normalizeGlyphHeight_positiveInt(self): result = normalizers.normalizeGlyphHeight(1) self.assertEqual(result, 1) def test_normalizeGlyphHeight_negativeInt(self): result = normalizers.normalizeGlyphHeight(-1) self.assertEqual(result, -1) def test_normalizeGlyphHeight_positiveFloat(self): result = normalizers.normalizeGlyphHeight(1.01) self.assertEqual(result, 1.01) def test_normalizeGlyphHeight_negativeFloat(self): result = normalizers.normalizeGlyphHeight(-1.01) self.assertEqual(result, -1.01) def test_normalizeGlyphHeight_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphHeight("1") # normalizeGlyphTopMargin def test_normalizeGlyphTopMargin_zero(self): result = normalizers.normalizeGlyphTopMargin(0) self.assertEqual(result, 0) def test_normalizeGlyphTopMargin_positiveInt(self): result = normalizers.normalizeGlyphTopMargin(1) self.assertEqual(result, 1) def test_normalizeGlyphTopMargin_negativeInt(self): result = normalizers.normalizeGlyphTopMargin(-1) self.assertEqual(result, -1) def test_normalizeGlyphTopMargin_positiveFloat(self): result = normalizers.normalizeGlyphTopMargin(1.01) self.assertEqual(result, 1.01) def test_normalizeGlyphTopMargin_negativeFloat(self): result = normalizers.normalizeGlyphTopMargin(-1.01) self.assertEqual(result, -1.01) def test_normalizeGlyphTopMargin_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphTopMargin("1") # normalizeGlyphFormatVersion def test_normalizeGlyphFormatVersion_int1(self): result = normalizers.normalizeGlyphFormatVersion(1) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeGlyphFormatVersion_int2(self): result = normalizers.normalizeGlyphFormatVersion(2) self.assertIsInstance(result, int) self.assertEqual(result, 2) def test_normalizeGlyphFormatVersion_int3(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphFormatVersion(3) def test_normalizeGlyphFormatVersion_float1(self): result = normalizers.normalizeGlyphFormatVersion(1.0) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeGlyphFormatVersion_float2(self): result = normalizers.normalizeGlyphFormatVersion(2.0) self.assertIsInstance(result, int) self.assertEqual(result, 2) def test_normalizeGlyphFormatVersion_float3(self): with self.assertRaises(ValueError): normalizers.normalizeGlyphFormatVersion(3.0) def test_normalizeGlyphFormatVersion_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphFormatVersion("1") # ------- # Contour # ------- # normalizeContour def test_normalizeContour_valid(self): from fontParts.base.contour import BaseContour contour, _ = self.objectGenerator("contour") result = normalizers.normalizeContour(contour) self.assertIsInstance(result, BaseContour) self.assertEqual(result, contour) def test_normalizeContour_notContour(self): with self.assertRaises(TypeError): normalizers.normalizeContour(123) # ----- # Point # ----- # normalizePoint def test_normalizePoint_valid(self): from fontParts.base.point import BasePoint point, _ = self.objectGenerator("point") result = normalizers.normalizePoint(point) self.assertIsInstance(result, BasePoint) self.assertEqual(result, point) def test_normalizePoint_notPoint(self): with self.assertRaises(TypeError): normalizers.normalizePoint(123) # normalizePointType def test_normalizePointType_move(self): result = normalizers.normalizePointType("move") self.assertIsInstance(result, str) self.assertEqual(result, u"move") def test_normalizePointType_Move(self): with self.assertRaises(ValueError): normalizers.normalizePointType("Move") def test_normalizePointType_MOVE(self): with self.assertRaises(ValueError): normalizers.normalizePointType("MOVE") def test_normalizePointType_line(self): result = normalizers.normalizePointType("line") self.assertIsInstance(result, str) self.assertEqual(result, u"line") def test_normalizePointType_Line(self): with self.assertRaises(ValueError): normalizers.normalizePointType("Line") def test_normalizePointType_LINE(self): with self.assertRaises(ValueError): normalizers.normalizePointType("LINE") def test_normalizePointType_offcurve(self): result = normalizers.normalizePointType("offcurve") self.assertIsInstance(result, str) self.assertEqual(result, u"offcurve") def test_normalizePointType_OffCurve(self): with self.assertRaises(ValueError): normalizers.normalizePointType("OffCurve") def test_normalizePointType_OFFCURVE(self): with self.assertRaises(ValueError): normalizers.normalizePointType("OFFCURVE") def test_normalizePointType_curve(self): result = normalizers.normalizePointType("curve") self.assertIsInstance(result, str) self.assertEqual(result, u"curve") def test_normalizePointType_Curve(self): with self.assertRaises(ValueError): normalizers.normalizePointType("Curve") def test_normalizePointType_CURVE(self): with self.assertRaises(ValueError): normalizers.normalizePointType("CURVE") def test_normalizePointType_qcurve(self): result = normalizers.normalizePointType("qcurve") self.assertIsInstance(result, str) self.assertEqual(result, u"qcurve") def test_normalizePointType_QOffCurve(self): with self.assertRaises(ValueError): normalizers.normalizePointType("QCurve") def test_normalizePointType_QOFFCURVE(self): with self.assertRaises(ValueError): normalizers.normalizePointType("QCURVE") def test_normalizePointType_unknown(self): with self.assertRaises(ValueError): normalizers.normalizePointType("unknonwn") def test_normalizePointType_notString(self): with self.assertRaises(TypeError): normalizers.normalizePointType(123) # normalizePointName def test_normalizePointName_valid(self): result = normalizers.normalizePointName("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizePointName_notString(self): with self.assertRaises(TypeError): normalizers.normalizePointName(123) def test_normalizePointName_notLongEnough(self): with self.assertRaises(ValueError): normalizers.normalizePointName("") # ------- # Segment # ------- # normalizeSegment def test_normalizeSegment_valid(self): from fontParts.base.segment import BaseSegment segment, _ = self.objectGenerator("segment") result = normalizers.normalizeSegment(segment) self.assertIsInstance(result, BaseSegment) self.assertEqual(result, segment) def test_normalizePoint_notContour(self): with self.assertRaises(TypeError): normalizers.normalizeSegment(123) # normalizeSegmentType def test_normalizeSegmentType_move(self): result = normalizers.normalizeSegmentType("move") self.assertIsInstance(result, str) self.assertEqual(result, u"move") def test_normalizeSegmentType_Move(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("Move") def test_normalizeSegmentType_MOVE(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("MOVE") def test_normalizeSegmentType_line(self): result = normalizers.normalizeSegmentType("line") self.assertIsInstance(result, str) self.assertEqual(result, u"line") def test_normalizeSegmentType_Line(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("Line") def test_normalizeSegmentType_LINE(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("LINE") def test_normalizeSegmentType_curve(self): result = normalizers.normalizeSegmentType("curve") self.assertIsInstance(result, str) self.assertEqual(result, u"curve") def test_normalizeSegmentType_OffCurve(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("Curve") def test_normalizeSegmentType_OFFCURVE(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("CURVE") def test_normalizeSegmentType_qcurve(self): result = normalizers.normalizeSegmentType("qcurve") self.assertIsInstance(result, str) self.assertEqual(result, u"qcurve") def test_normalizeSegmentType_QOffCurve(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("QCurve") def test_normalizeSegmentType_QOFFCURVE(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("QCURVE") def test_normalizeSegmentType_unknown(self): with self.assertRaises(ValueError): normalizers.normalizeSegmentType("offcurve") def test_normalizeSegmentType_notString(self): with self.assertRaises(TypeError): normalizers.normalizeSegmentType(123) # ------ # BPoint # ------ # normalizeBPoint def test_normalizeBPoint_valid(self): from fontParts.base.bPoint import BaseBPoint bPoint, _ = self.objectGenerator("bPoint") result = normalizers.normalizeBPoint(bPoint) self.assertIsInstance(result, BaseBPoint) self.assertEqual(result, bPoint) def test_normalizeBPoint_notBPoint(self): with self.assertRaises(TypeError): normalizers.normalizeBPoint(123) # normalizeBPointType def test_normalizeBPointType_corner(self): result = normalizers.normalizeBPointType("corner") self.assertIsInstance(result, str) self.assertEqual(result, u"corner") def test_normalizeBPointType_Corner(self): with self.assertRaises(ValueError): normalizers.normalizeBPointType("Corner") def test_normalizeBPointType_CORNER(self): with self.assertRaises(ValueError): normalizers.normalizeBPointType("CORNER") def test_normalizeBPointType_curve(self): result = normalizers.normalizeBPointType("curve") self.assertIsInstance(result, str) self.assertEqual(result, u"curve") def test_normalizeBPointType_OffCurve(self): with self.assertRaises(ValueError): normalizers.normalizeBPointType("Curve") def test_normalizeBPointType_OFFCURVE(self): with self.assertRaises(ValueError): normalizers.normalizeBPointType("CURVE") def test_normalizeBPointType_unknown(self): with self.assertRaises(ValueError): normalizers.normalizeBPointType("offcurve") def test_normalizeBPointType_notString(self): with self.assertRaises(TypeError): normalizers.normalizeBPointType(123) # --------- # Component # --------- # normalizeComponent def test_normalizeComponent_valid(self): from fontParts.base.component import BaseComponent component, _ = self.objectGenerator("component") result = normalizers.normalizeComponent(component) self.assertIsInstance(result, BaseComponent) self.assertEqual(result, component) def test_normalizeComponent_notComponent(self): with self.assertRaises(TypeError): normalizers.normalizeComponent(123) # normalizeComponentScale def test_normalizeComponentScale_tupleZero(self): result = normalizers.normalizeComponentScale((0, 0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (0, 0)) def test_normalizeComponentScale_tuplePositiveInt(self): result = normalizers.normalizeComponentScale((2, 2)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (2.0, 2.0)) def test_normalizeComponentScale_tupleNegativeInt(self): result = normalizers.normalizeComponentScale((-2, -2)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeComponentScale_tuplePositiveFloat(self): result = normalizers.normalizeComponentScale((2.0, 2.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (2.0, 2.0)) def test_normalizeComponentScale_tupleNegativeFloat(self): result = normalizers.normalizeComponentScale((-2.0, -2.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeComponentScale_listZero(self): result = normalizers.normalizeComponentScale([0, 0]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (0, 0)) def test_normalizeComponentScale_listPositiveInt(self): result = normalizers.normalizeComponentScale([2, 2]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (2.0, 2.0)) def test_normalizeComponentScale_listNegativeInt(self): result = normalizers.normalizeComponentScale([-2, -2]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeComponentScale_listPositiveFloat(self): result = normalizers.normalizeComponentScale([2.0, 2.0]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (2.0, 2.0)) def test_normalizeComponentScale_listNegativeFloat(self): result = normalizers.normalizeComponentScale([-2.0, -2.0]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeComponentScale_notTupleOrList(self): with self.assertRaises(TypeError): normalizers.normalizeComponentScale("2, 2") def test_normalizeComponentScale_numberNotNumber(self): with self.assertRaises(TypeError): normalizers.normalizeComponentScale((2, "2")) def test_normalizeComponentScale_notNumberNumber(self): with self.assertRaises(TypeError): normalizers.normalizeComponentScale(("2", 2)) def test_normalizeComponentScale_notEnough(self): with self.assertRaises(ValueError): normalizers.normalizeComponentScale((2,)) def test_normalizeComponentScale_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeComponentScale((2, 2, 2)) # ------ # Anchor # ------ # normalizeAnchor def test_normalizeAnchor_valid(self): from fontParts.base.anchor import BaseAnchor anchor, _ = self.objectGenerator("anchor") result = normalizers.normalizeAnchor(anchor) self.assertIsInstance(result, BaseAnchor) self.assertEqual(result, anchor) def test_normalizeAnchor_notAnchor(self): with self.assertRaises(TypeError): normalizers.normalizeAnchor(123) # normalizeAnchorName def test_normalizeAnchorName_valid(self): result = normalizers.normalizeAnchorName("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizeAnchorName_notString(self): with self.assertRaises(TypeError): normalizers.normalizeAnchorName(123) def test_normalizeAnchorName_notLongEnough(self): with self.assertRaises(ValueError): normalizers.normalizeAnchorName("") # --------- # Guideline # --------- # normalizeGuideline def test_normalizeGuideline_valid(self): from fontParts.base.guideline import BaseGuideline guideline, _ = self.objectGenerator("guideline") result = normalizers.normalizeGuideline(guideline) self.assertIsInstance(result, BaseGuideline) self.assertEqual(result, guideline) def test_normalizeGuideline_notGuideline(self): with self.assertRaises(TypeError): normalizers.normalizeGuideline(123) # normalizeGuidelineName def test_normalizeGuidelineName_valid(self): result = normalizers.normalizeGuidelineName("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizeGuidelineName_notString(self): with self.assertRaises(TypeError): normalizers.normalizeGuidelineName(123) def test_normalizeGuidelineName_notLongEnough(self): with self.assertRaises(ValueError): normalizers.normalizeGuidelineName("") # ------- # Generic # ------- # normalizeBoolean def test_normalizeBoolean_true(self): result = normalizers.normalizeBoolean(True) self.assertIsInstance(result, bool) self.assertEqual(result, True) def test_normalizeBoolean_false(self): result = normalizers.normalizeBoolean(False) self.assertIsInstance(result, bool) self.assertEqual(result, False) def test_normalizeBoolean_1(self): result = normalizers.normalizeBoolean(1) self.assertIsInstance(result, bool) self.assertEqual(result, True) def test_normalizeBoolean_0(self): result = normalizers.normalizeBoolean(0) self.assertIsInstance(result, bool) self.assertEqual(result, False) def test_normalizeBoolean_10(self): with self.assertRaises(ValueError): normalizers.normalizeBoolean(10) def test_normalizeBoolean_string(self): with self.assertRaises(ValueError): normalizers.normalizeBoolean("True") # normalizeIndex def test_normalizeIndex_zero(self): result = normalizers.normalizeIndex(0) self.assertIsInstance(result, int) self.assertEqual(result, 0) def test_normalizeIndex_positiveInt(self): result = normalizers.normalizeIndex(1) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeIndex_negativeInt(self): result = normalizers.normalizeIndex(-1) self.assertIsInstance(result, int) self.assertEqual(result, -1) def test_normalizeIndex_notInt(self): with self.assertRaises(TypeError): normalizers.normalizeIndex(1.0) # normalizeIdentifier def test_normalizeIdentifier_none(self): result = normalizers.normalizeIdentifier(None) self.assertEqual(result, None) def test_normalizeIdentifier_stringMinimumLength(self): result = normalizers.normalizeIdentifier("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizeIdentifier_stringMaximumLength(self): result = normalizers.normalizeIdentifier("A" * 100) self.assertIsInstance(result, str) self.assertEqual(result, u"A" * 100) def test_normalizeIdentifier_stringMinimumCharacter(self): result = normalizers.normalizeIdentifier(chr(0x20)) self.assertIsInstance(result, str) self.assertEqual(result, chr(0x20)) def test_normalizeIdentifier_stringMaximumCharacter(self): result = normalizers.normalizeIdentifier(chr(0x7E)) self.assertIsInstance(result, str) self.assertEqual(result, chr(0x7E)) def test_normalizeIdentifier_stringTooShort(self): with self.assertRaises(ValueError): normalizers.normalizeIdentifier("") def test_normalizeIdentifier_stringTooLong(self): with self.assertRaises(ValueError): normalizers.normalizeIdentifier("A" * 101) def test_normalizeIdentifier_stringBeforeMinimumCharacter(self): with self.assertRaises(ValueError): normalizers.normalizeIdentifier(chr(0x20 - 1)) def test_normalizeIdentifier_stringAfterMaximumCharacter(self): with self.assertRaises(ValueError): normalizers.normalizeIdentifier(chr(0x7E + 1)) def test_normalizeIdentifier_notString(self): with self.assertRaises(TypeError): normalizers.normalizeIdentifier(1) # normalizeX def test_normalizeX_zero(self): result = normalizers.normalizeX(0) self.assertIsInstance(result, int) self.assertEqual(result, 0) def test_normalizeX_positiveInt(self): result = normalizers.normalizeX(1) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeX_negativeInt(self): result = normalizers.normalizeX(-1) self.assertIsInstance(result, int) self.assertEqual(result, -1) def test_normalizeX_positiveFloat(self): result = normalizers.normalizeX(1.0) self.assertIsInstance(result, float) self.assertEqual(result, 1.0) def test_normalizeX_negativeFloat(self): result = normalizers.normalizeX(-1.0) self.assertIsInstance(result, float) self.assertEqual(result, -1.0) def test_normalizeX_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeX("1") # normalizeY def test_normalizeY_zero(self): result = normalizers.normalizeY(0) self.assertIsInstance(result, int) self.assertEqual(result, 0) def test_normalizeY_positiveInt(self): result = normalizers.normalizeY(1) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeY_negativeInt(self): result = normalizers.normalizeY(-1) self.assertIsInstance(result, int) self.assertEqual(result, -1) def test_normalizeY_positiveFloat(self): result = normalizers.normalizeY(1.0) self.assertIsInstance(result, float) self.assertEqual(result, 1.0) def test_normalizeY_negativeFloat(self): result = normalizers.normalizeY(-1.0) self.assertIsInstance(result, float) self.assertEqual(result, -1.0) def test_normalizeY_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeY("1") # normalizeCoordinateTuple def test_normalizeCoordinateTuple_list(self): result = normalizers.normalizeCoordinateTuple([1, 1]) self.assertIsInstance(result, tuple) self.assertEqual(result, (1, 1)) def test_normalizeCoordinateTuple_zero(self): result = normalizers.normalizeCoordinateTuple((0, 0)) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0)) def test_normalizeCoordinateTuple_positiveInt(self): result = normalizers.normalizeCoordinateTuple((1, 1)) self.assertIsInstance(result, tuple) self.assertEqual(result, (1, 1)) def test_normalizeCoordinateTuple_negativeInt(self): result = normalizers.normalizeCoordinateTuple((-1, -1)) self.assertIsInstance(result, tuple) self.assertEqual(result, (-1, -1)) def test_normalizeCoordinateTuple_positiveFloat(self): result = normalizers.normalizeCoordinateTuple((1.0, 1.0)) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 1.0)) def test_normalizeCoordinateTuple_negativeFloat(self): result = normalizers.normalizeCoordinateTuple((-1.0, -1.0)) self.assertIsInstance(result, tuple) self.assertEqual(result, (-1.0, -1.0)) def test_normalizeCoordinateTuple_numberNotNumber(self): with self.assertRaises(TypeError): normalizers.normalizeCoordinateTuple((1, "1")) def test_normalizeCoordinateTuple_notNumberNumber(self): with self.assertRaises(TypeError): normalizers.normalizeCoordinateTuple(("1", 1)) def test_normalizeCoordinateTuple_notEnough(self): with self.assertRaises(ValueError): normalizers.normalizeCoordinateTuple((1,)) def test_normalizeCoordinateTuple_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeCoordinateTuple((1, 1, 1)) # normalizeBoundingBox def test_normalizeBoundingBox_tuple(self): result = normalizers.normalizeBoundingBox((1, 2, 3, 4)) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 2.0, 3.0, 4.0)) def test_normalizeBoundingBox_list(self): result = normalizers.normalizeBoundingBox([1, 2, 3, 4]) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 2.0, 3.0, 4.0)) def test_normalizeBoundingBox_int(self): result = normalizers.normalizeBoundingBox((1, 2, 3, 4)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (1.0, 2.0, 3.0, 4.0)) def test_normalizeBoundingBox_float(self): result = normalizers.normalizeBoundingBox((1.0, 2.0, 3.0, 4.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (1.0, 2.0, 3.0, 4.0)) def test_normalizeBoundingBox_notEnough(self): with self.assertRaises(ValueError): normalizers.normalizeBoundingBox((1, 2, 3)) def test_normalizeBoundingBox_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeBoundingBox((1, 2, 3, 4, 5)) def test_normalizeBoundingBox_notListOrTuple(self): with self.assertRaises(TypeError): normalizers.normalizeBoundingBox("1 2 3 4") def test_normalizeBoundingBox_invalidMember(self): with self.assertRaises(TypeError): normalizers.normalizeBoundingBox((1, 2, "3", 4)) def test_normalizeBoundingBox_xMinSameAsXMax(self): result = normalizers.normalizeBoundingBox((1, 2, 1, 4)) self.assertEqual(result, (1.0, 2.0, 1.0, 4.0)) def test_normalizeBoundingBox_yMinSameAsYMax(self): result = normalizers.normalizeBoundingBox((1, 2, 3, 2)) self.assertEqual(result, (1.0, 2.0, 3.0, 2.0)) def test_normalizeBoundingBox_xMaxLessThanXMin(self): with self.assertRaises(ValueError): normalizers.normalizeBoundingBox((1, 2, 0, 4)) def test_normalizeBoundingBox_yMaxLessThanYMin(self): with self.assertRaises(ValueError): normalizers.normalizeBoundingBox((1, 2, 3, 1)) # normalizeArea def test_normalizeArea_zero(self): result = normalizers.normalizeArea(0) self.assertIsInstance(result, float) self.assertEqual(result, 0) def test_normalizeArea_positiveInt(self): result = normalizers.normalizeArea(1) self.assertIsInstance(result, float) self.assertEqual(result, 1.0) def test_normalizeArea_positiveFloat(self): result = normalizers.normalizeArea(1.0) self.assertIsInstance(result, float) self.assertEqual(result, 1.0) def test_normalizeArea_negativeInt(self): with self.assertRaises(ValueError): normalizers.normalizeArea(-1) def test_normalizeArea_negativeFloat(self): with self.assertRaises(ValueError): normalizers.normalizeArea(-1.0) def test_normalizeArea_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeArea("1") # normalizeColor def test_normalizeColor_color(self): from fontParts.base.color import Color result = normalizers.normalizeColor(Color((0, 0, 0, 0))) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0, 0, 0)) def test_normalizeColor_tuple(self): result = normalizers.normalizeColor((0, 0, 0, 0)) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0, 0, 0)) def test_normalizeColor_list(self): result = normalizers.normalizeColor([0, 0, 0, 0]) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0, 0, 0)) def test_normalizeColor_ints(self): result = normalizers.normalizeColor((1, 1, 1, 1)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (1.0, 1.0, 1.0, 1.0)) def test_normalizeColor_floats(self): result = normalizers.normalizeColor((1.0, 1.0, 1.0, 1.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (1.0, 1.0, 1.0, 1.0)) def test_normalizeColor_tooSmall(self): with self.assertRaises(ValueError): normalizers.normalizeColor((-1, 1, 1, 1)) with self.assertRaises(ValueError): normalizers.normalizeColor((1, -1, 1, 1)) with self.assertRaises(ValueError): normalizers.normalizeColor((1, 1, -1, 1)) with self.assertRaises(ValueError): normalizers.normalizeColor((1, 1, 1, -1)) def test_normalizeColor_tooLarge(self): with self.assertRaises(ValueError): normalizers.normalizeColor((2, 1, 1, 1)) with self.assertRaises(ValueError): normalizers.normalizeColor((1, 2, 1, 1)) with self.assertRaises(ValueError): normalizers.normalizeColor((1, 1, 2, 1)) with self.assertRaises(ValueError): normalizers.normalizeColor((1, 1, 1, 2)) def test_normalizeColor_notEnough(self): with self.assertRaises(ValueError): normalizers.normalizeColor((1, 1, 1)) def test_normalizeColor_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeColor((1, 1, 1, 1, 1)) def test_normalizeColor_notTupleOrList(self): with self.assertRaises(TypeError): normalizers.normalizeColor("1,1,1,1") def test_normalizeColor_invalidMember(self): with self.assertRaises(TypeError): normalizers.normalizeColor((1, "1", 1, 1)) # normalizeGlyphNote def test_normalizeGlyphNote_string(self): result = normalizers.normalizeGlyphNote("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizeGlyphNote_emptyString(self): result = normalizers.normalizeGlyphNote("") self.assertIsInstance(result, str) self.assertEqual(result, u"") def test_normalizeGlyphNote_notString(self): with self.assertRaises(TypeError): normalizers.normalizeGlyphNote(123) # normalizeFilePath def test_normalizeFilePath_string(self): result = normalizers.normalizeFilePath("A") self.assertIsInstance(result, str) self.assertEqual(result, u"A") def test_normalizeFilePath_emptyString(self): result = normalizers.normalizeFilePath("") self.assertIsInstance(result, str) self.assertEqual(result, u"") def test_normalizeFilePath_notString(self): with self.assertRaises(TypeError): normalizers.normalizeFilePath(123) # normalizeInterpolationFactor def test_normalizeInterpolationFactor_zero(self): result = normalizers.normalizeInterpolationFactor(0) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0)) def test_normalizeInterpolationFactor_positiveInt(self): result = normalizers.normalizeInterpolationFactor(1) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 1.0)) def test_normalizeInterpolationFactor_negativeInt(self): result = normalizers.normalizeInterpolationFactor(-1) self.assertIsInstance(result, tuple) self.assertEqual(result, (-1.0, -1.0)) def test_normalizeInterpolationFactor_positiveFloat(self): result = normalizers.normalizeInterpolationFactor(1.0) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 1.0)) def test_normalizeInterpolationFactor_negativeFloat(self): result = normalizers.normalizeInterpolationFactor(-1.0) self.assertIsInstance(result, tuple) self.assertEqual(result, (-1.0, -1.0)) def test_normalizeInterpolationFactor_tupleZero(self): result = normalizers.normalizeInterpolationFactor((0, 0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (0, 0)) def test_normalizeInterpolationFactor_tuplePositiveInt(self): result = normalizers.normalizeInterpolationFactor((2, 2)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (2.0, 2.0)) def test_normalizeInterpolationFactor_tupleNegativeInt(self): result = normalizers.normalizeInterpolationFactor((-2, -2)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeInterpolationFactor_tuplePositiveFloat(self): result = normalizers.normalizeInterpolationFactor((2.0, 2.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (2.0, 2.0)) def test_normalizeInterpolationFactor_tupleNegativeFloat(self): result = normalizers.normalizeInterpolationFactor((-2.0, -2.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeInterpolationFactor_listZero(self): result = normalizers.normalizeInterpolationFactor([0, 0]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (0, 0)) def test_normalizeInterpolationFactor_listPositiveInt(self): result = normalizers.normalizeInterpolationFactor([2, 2]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (2.0, 2.0)) def test_normalizeInterpolationFactor_listNegativeInt(self): result = normalizers.normalizeInterpolationFactor([-2, -2]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeInterpolationFactor_listPositiveFloat(self): result = normalizers.normalizeInterpolationFactor([2.0, 2.0]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (2.0, 2.0)) def test_normalizeInterpolationFactor_listNegativeFloat(self): result = normalizers.normalizeInterpolationFactor([-2.0, -2.0]) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeInterpolationFactor_notTupleOrList(self): with self.assertRaises(TypeError): normalizers.normalizeInterpolationFactor("2, 2") def test_normalizeInterpolationFactor_numberNotNumber(self): with self.assertRaises(TypeError): normalizers.normalizeInterpolationFactor((2, "2")) def test_normalizeInterpolationFactor_notNumberNumber(self): with self.assertRaises(TypeError): normalizers.normalizeInterpolationFactor(("2", 2)) def test_normalizeInterpolationFactor_notEnough(self): with self.assertRaises(ValueError): normalizers.normalizeInterpolationFactor((2,)) def test_normalizeInterpolationFactor_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeInterpolationFactor((2, 2, 2)) # normalizeRotationAngle def test_normalizeRotationAngle_zero(self): result = normalizers.normalizeRotationAngle(0) self.assertIsInstance(result, float) self.assertEqual(result, 0) def test_normalizeRotationAngle_positiveInt(self): result = normalizers.normalizeRotationAngle(1) self.assertIsInstance(result, float) self.assertEqual(result, 1.0) def test_normalizeRotationAngle_negativeInt(self): result = normalizers.normalizeRotationAngle(-1) self.assertIsInstance(result, float) self.assertEqual(result, 359.0) def test_normalizeRotationAngle_positiveFloat(self): result = normalizers.normalizeRotationAngle(1.0) self.assertIsInstance(result, float) self.assertEqual(result, 1.0) def test_normalizeRotationAngle_negativeFloat(self): result = normalizers.normalizeRotationAngle(-1.0) self.assertIsInstance(result, float) self.assertEqual(result, 359.0) def test_normalizeRotationAngle_maximum(self): result = normalizers.normalizeRotationAngle(360) self.assertIsInstance(result, float) self.assertEqual(result, 360.0) def test_normalizeRotationAngle_minimum(self): result = normalizers.normalizeRotationAngle(-360) self.assertIsInstance(result, float) self.assertEqual(result, 0) def test_normalizeRotationAngle_moreThanMaximum(self): with self.assertRaises(ValueError): normalizers.normalizeRotationAngle(361) def test_normalizeRotationAngle_lessThanMaximum(self): with self.assertRaises(ValueError): normalizers.normalizeRotationAngle(-361) def test_normalizeRotationAngle_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeRotationAngle("1") # --------------- # Transformations # --------------- # normalizeTransformationMatrix def test_normalizeTransformationMatrix_tuple(self): result = normalizers.normalizeTransformationMatrix((1, 2, 3, 4, 5, 6)) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)) def test_normalizeTransformationMatrix_list(self): result = normalizers.normalizeTransformationMatrix([1, 2, 3, 4, 5, 6]) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)) def test_normalizeTransformationMatrix_positiveInts(self): result = normalizers.normalizeTransformationMatrix((1, 2, 3, 4, 5, 6)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)) def test_normalizeTransformationMatrix_positiveFloats(self): result = normalizers.normalizeTransformationMatrix((1.0, 2.0, 3.0, 4.0, 5.0, 6.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)) def test_normalizeTransformationMatrix_negativeInts(self): result = normalizers.normalizeTransformationMatrix((-1, -2, -3, -4, -5, -6)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-1.0, -2.0, -3.0, -4.0, -5.0, -6.0)) def test_normalizeTransformationMatrix_negativeFloats(self): result = normalizers.normalizeTransformationMatrix((-1.0, -2.0, -3.0, -4.0, -5.0, -6.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-1.0, -2.0, -3.0, -4.0, -5.0, -6.0)) def test_normalizeTransformationMatrix_notEnough(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationMatrix((1, 2, 3, 4, 5)) def test_normalizeTransformationMatrix_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationMatrix((1, 2, 3, 4, 5, 6, 7)) def test_normalizeTransformationMatrix_notTupleOrList(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationMatrix("1 2 3 4 5 6") def test_normalizeTransformationMatrix_invalidMember(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationMatrix((1, 2, 3, "4", 5, 6)) # normalizeTransformationOffset def test_normalizeTransformationOffset_tupleZero(self): result = normalizers.normalizeTransformationOffset((0, 0)) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0)) def test_normalizeTransformationOffset_tuplePositiveInt(self): result = normalizers.normalizeTransformationOffset((2, 2)) self.assertIsInstance(result, tuple) self.assertEqual(result, (2.0, 2.0)) def test_normalizeTransformationOffset_tupleNegativeInt(self): result = normalizers.normalizeTransformationOffset((-2, -2)) self.assertIsInstance(result, tuple) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeTransformationOffset_tuplePositiveFloat(self): result = normalizers.normalizeTransformationOffset((2.0, 2.0)) self.assertIsInstance(result, tuple) self.assertEqual(result, (2.0, 2.0)) def test_normalizeTransformationOffset_tupleNegativeFloat(self): result = normalizers.normalizeTransformationOffset((-2.0, -2.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeTransformationOffset_listZero(self): result = normalizers.normalizeTransformationOffset([0, 0]) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0)) def test_normalizeTransformationOffset_listPositiveInt(self): result = normalizers.normalizeTransformationOffset([2, 2]) self.assertIsInstance(result, tuple) self.assertEqual(result, (2.0, 2.0)) def test_normalizeTransformationOffset_listNegativeInt(self): result = normalizers.normalizeTransformationOffset([-2, -2]) self.assertIsInstance(result, tuple) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeTransformationOffset_listPositiveFloat(self): result = normalizers.normalizeTransformationOffset([2.0, 2.0]) self.assertIsInstance(result, tuple) self.assertEqual(result, (2.0, 2.0)) def test_normalizeTransformationOffset_listNegativeFloat(self): result = normalizers.normalizeTransformationOffset([-2.0, -2.0]) self.assertIsInstance(result, tuple) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeTransformationOffset_notTupleOrList(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationOffset("2, 2") def test_normalizeTransformationOffset_numberNotNumber(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationOffset((2, "2")) def test_normalizeTransformationOffset_notNumberNumber(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationOffset(("2", 2)) def test_normalizeTransformationOffset_notEnough(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationOffset((2,)) def test_normalizeTransformationOffset_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationOffset((2, 2, 2)) # normalizeTransformationSkewAngle def test_normalizeTransformationSkewAngle_int(self): result = normalizers.normalizeTransformationSkewAngle(1) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 0)) def test_normalizeTransformationSkewAngle_float(self): result = normalizers.normalizeTransformationSkewAngle(1.0) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 0)) def test_normalizeTransformationSkewAngle_list(self): result = normalizers.normalizeTransformationSkewAngle([1, 2]) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 2.0)) def test_normalizeTransformationSkewAngle_tuple(self): result = normalizers.normalizeTransformationSkewAngle((1, 2)) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 2.0)) def test_normalizeTransformationSkewAngle_zero(self): result = normalizers.normalizeTransformationSkewAngle((0, 0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (0, 0)) def test_normalizeTransformationSkewAngle_positiveInts(self): result = normalizers.normalizeTransformationSkewAngle((1, 2)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (1.0, 2.0)) def test_normalizeTransformationSkewAngle_negativeInts(self): result = normalizers.normalizeTransformationSkewAngle((-1, -2)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (359.0, 358.0)) def test_normalizeTransformationSkewAngle_positiveFloats(self): result = normalizers.normalizeTransformationSkewAngle((1.0, 2.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (1.0, 2.0)) def test_normalizeTransformationSkewAngle_negativeFloats(self): result = normalizers.normalizeTransformationSkewAngle((-1.0, -2.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (359.0, 358.0)) def test_normalizeTransformationSkewAngle_tooLow(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationSkewAngle((-361, -361)) def test_normalizeTransformationSkewAngle_tooHigh(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationSkewAngle((361, 361)) def test_normalizeTransformationSkewAngle_numberNotNumber(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationSkewAngle((1, "2")) def test_normalizeTransformationSkewAngle_notNumberNumber(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationSkewAngle(("1", 1)) def test_normalizeTransformationSkewAngle_tooFew(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationSkewAngle(tuple()) def test_normalizeTransformationSkewAngle_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationSkewAngle((1, 2, 3)) def test_normalizeTransformationSkewAngle_notTupleOrList(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationSkewAngle("1") # normalizeTransformationScale def test_normalizeTransformationScale_int(self): result = normalizers.normalizeTransformationScale(1) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 1.0)) def test_normalizeTransformationScale_float(self): result = normalizers.normalizeTransformationScale(1.0) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 1.0)) def test_normalizeTransformationScale_list(self): result = normalizers.normalizeTransformationScale([1, 2]) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 2.0)) def test_normalizeTransformationScale_tuple(self): result = normalizers.normalizeTransformationScale((1, 2)) self.assertIsInstance(result, tuple) self.assertEqual(result, (1.0, 2.0)) def test_normalizeTransformationScale_tupleZero(self): result = normalizers.normalizeTransformationScale((0, 0)) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0)) def test_normalizeTransformationScale_tuplePositiveInt(self): result = normalizers.normalizeTransformationScale((2, 2)) self.assertIsInstance(result, tuple) self.assertEqual(result, (2.0, 2.0)) def test_normalizeTransformationScale_tupleNegativeInt(self): result = normalizers.normalizeTransformationScale((-2, -2)) self.assertIsInstance(result, tuple) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeTransformationScale_tuplePositiveFloat(self): result = normalizers.normalizeTransformationScale((2.0, 2.0)) self.assertIsInstance(result, tuple) self.assertEqual(result, (2.0, 2.0)) def test_normalizeTransformationScale_tupleNegativeFloat(self): result = normalizers.normalizeTransformationScale((-2.0, -2.0)) self.assertIsInstance(result, tuple) for i in result: self.assertIsInstance(i, float) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeTransformationScale_listZero(self): result = normalizers.normalizeTransformationScale([0, 0]) self.assertIsInstance(result, tuple) self.assertEqual(result, (0, 0)) def test_normalizeTransformationScale_listPositiveInt(self): result = normalizers.normalizeTransformationScale([2, 2]) self.assertIsInstance(result, tuple) self.assertEqual(result, (2.0, 2.0)) def test_normalizeTransformationScale_listNegativeInt(self): result = normalizers.normalizeTransformationScale([-2, -2]) self.assertIsInstance(result, tuple) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeTransformationScale_listPositiveFloat(self): result = normalizers.normalizeTransformationScale([2.0, 2.0]) self.assertIsInstance(result, tuple) self.assertEqual(result, (2.0, 2.0)) def test_normalizeTransformationScale_listNegativeFloat(self): result = normalizers.normalizeTransformationScale([-2.0, -2.0]) self.assertIsInstance(result, tuple) self.assertEqual(result, (-2.0, -2.0)) def test_normalizeTransformationScale_notTupleOrList(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationScale("2, 2") def test_normalizeTransformationScale_numberNotNumber(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationScale((2, "2")) def test_normalizeTransformationScale_notNumberNumber(self): with self.assertRaises(TypeError): normalizers.normalizeTransformationScale(("2", 2)) def test_normalizeTransformationScale_notEnough(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationScale((2,)) def test_normalizeTransformationScale_tooMany(self): with self.assertRaises(ValueError): normalizers.normalizeTransformationScale((2, 2, 2)) # normalizeVisualRounding def test_normalizeVisualRounding_int(self): result = normalizers.normalizeVisualRounding(1) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeVisualRounding_float(self): result = normalizers.normalizeVisualRounding(1.0) self.assertIsInstance(result, int) self.assertEqual(result, 1) def test_normalizeVisualRounding_half(self): result = normalizers.normalizeVisualRounding(1.5) self.assertIsInstance(result, int) self.assertEqual(result, 2) def test_normalizeVisualRounding_half_even(self): result = normalizers.normalizeVisualRounding(2.5) self.assertIsInstance(result, int) self.assertEqual(result, 3) def test_normalizeVisualRounding_notNumber(self): with self.assertRaises(TypeError): normalizers.normalizeVisualRounding("1") robotools-fontParts-26e8b8c/Lib/fontParts/test/test_point.py000066400000000000000000000556501477533125200243700ustar00rootroot00000000000000import unittest import collections from fontParts.base import FontPartsError class TestPoint(unittest.TestCase): def getPoint_generic(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") point = contour.points[1] point.smooth = True return point # ---- # repr # ---- def test_reprContents(self): point = self.getPoint_generic() value = point._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_withName(self): point = self.getPoint_withName() value = point._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_isSmooth(self): point = self.getPoint_generic() point.smooth = True value = point._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) def test_reprContents_noContour(self): point, _ = self.objectGenerator("point") value = point._reprContents() self.assertIsInstance(value, list) for i in value: self.assertIsInstance(i, str) # ------- # Parents # ------- def test_get_parent_font(self): font, _ = self.objectGenerator("font") layer = font.newLayer("L") glyph = layer.newGlyph("X") contour, _ = self.objectGenerator("contour") contour.appendPoint((10, 20)) glyph.appendContour(contour) contour = glyph.contours[0] point = contour.points[0] self.assertIsNotNone(point.font) self.assertEqual( point.font, font ) def test_get_parent_noFont(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") contour, _ = self.objectGenerator("contour") contour.appendPoint((10, 20)) glyph.appendContour(contour) contour = glyph.contours[0] point = contour.points[0] self.assertIsNone(point.font) def test_get_parent_layer(self): layer, _ = self.objectGenerator("layer") glyph = layer.newGlyph("X") contour, _ = self.objectGenerator("contour") contour.appendPoint((10, 20)) glyph.appendContour(contour) contour = glyph.contours[0] point = contour.points[0] self.assertIsNotNone(point.layer) self.assertEqual( point.layer, layer ) def test_get_parent_noLayer(self): glyph, _ = self.objectGenerator("glyph") contour, _ = self.objectGenerator("contour") contour.appendPoint((10, 20)) glyph.appendContour(contour) contour = glyph.contours[0] point = contour.points[0] self.assertIsNone(point.font) self.assertIsNone(point.layer) def test_get_parent_glyph(self): glyph, _ = self.objectGenerator("glyph") contour, _ = self.objectGenerator("contour") contour.appendPoint((10, 20)) glyph.appendContour(contour) contour = glyph.contours[0] point = contour.points[0] self.assertIsNotNone(point.glyph) self.assertEqual( point.glyph, glyph ) def test_get_parent_noGlyph(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((10, 20)) point = contour.points[0] self.assertIsNone(point.glyph) def test_get_parent_contour(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((10, 20)) point = contour.points[0] self.assertIsNotNone(point.contour) self.assertEqual( point.contour, contour ) def test_get_parent_noContour(self): point, _ = self.objectGenerator("point") self.assertIsNone(point.contour) def test_get_parent_segment(self): point, _ = self.objectGenerator("point") with self.assertRaises(AttributeError): point.segment def test_set_parent_contour(self): contour, _ = self.objectGenerator("contour") point, _ = self.objectGenerator("point") point.contour = contour self.assertIsNotNone(point.contour) self.assertEqual( point.contour, contour ) def test_set_already_set_parent_contour(self): contour, _ = self.objectGenerator("contour") contour.appendPoint((10, 20)) point = contour.points[0] contourOther, _ = self.objectGenerator("contour") with self.assertRaises(AssertionError): point.contour = contourOther def test_set_parent_contour_none(self): point, _ = self.objectGenerator("point") point.contour = None self.assertIsNone(point.contour) def test_get_parent_glyph_noContour(self): point, _ = self.objectGenerator("point") self.assertIsNone(point.glyph) def test_get_parent_layer_noContour(self): point, _ = self.objectGenerator("point") self.assertIsNone(point.layer) def test_get_parent_font_noContour(self): point, _ = self.objectGenerator("point") self.assertIsNone(point.font) # ---------- # Attributes # ---------- # type def test_get_type(self): point = self.getPoint_generic() self.assertEqual( point.type, "line" ) def test_set_move(self): point = self.getPoint_generic() point.type = "move" self.assertEqual( point.type, "move" ) def test_set_curve(self): point = self.getPoint_generic() point.type = "curve" self.assertEqual( point.type, "curve" ) def test_set_wcurve(self): point = self.getPoint_generic() point.type = "qcurve" self.assertEqual( point.type, "qcurve" ) def test_set_offcurve(self): point = self.getPoint_generic() point.type = "offcurve" self.assertEqual( point.type, "offcurve" ) def test_set_invalid_point_type_string(self): point = self.getPoint_generic() with self.assertRaises(ValueError): point.type = "xxx" def test_set_invalid_point_type_int(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.type = 123 # smooth def test_get_smooth(self): point = self.getPoint_generic() self.assertEqual( point.smooth, True ) def test_set_smooth_valid(self): point = self.getPoint_generic() point.smooth = True self.assertEqual( point.smooth, True ) def test_set_smooth_invalid(self): point = self.getPoint_generic() with self.assertRaises(ValueError): point.smooth = "smooth" # x def test_get_x(self): point = self.getPoint_generic() self.assertEqual( point.x, 101 ) def test_set_x_valid_int(self): point = self.getPoint_generic() point.x = 100 self.assertEqual( point.x, 100 ) def test_set_x_valid_float(self): point = self.getPoint_generic() point.x = 100.5 self.assertEqual( point.x, 100.5 ) def test_set_x_invalidType(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.x = "100" # y def test_get_y(self): point = self.getPoint_generic() self.assertEqual( point.y, 202 ) def test_set_y_valid_int(self): point = self.getPoint_generic() point.y = 200 self.assertEqual( point.y, 200 ) def test_set_y_valid_float(self): point = self.getPoint_generic() point.y = 200.5 self.assertEqual( point.y, 200.5 ) def test_set_y_invalidType(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.y = "200" # -------------- # Identification # -------------- # index def getPoint_noParentContour(self): point, _ = self.objectGenerator("point") point.x = 101 point.y = 202 return point def test_get_index(self): point = self.getPoint_generic() self.assertEqual( point.index, 1 ) def test_get_index_noParentContour(self): point = self.getPoint_noParentContour() self.assertEqual( point.index, None ) def test_set_index(self): point = self.getPoint_generic() with self.assertRaises(FontPartsError): point.index = 0 # name def getPoint_withName(self): point = self.getPoint_generic() point.name = "P" return point def test_get_name_noName(self): point = self.getPoint_generic() self.assertEqual( point.name, None ) def test_get_name_hasName(self): point = self.getPoint_withName() self.assertEqual( point.name, "P" ) def test_set_name_valid_str(self): point = self.getPoint_generic() point.name = "P" self.assertEqual( point.name, "P" ) def test_set_name_valid_none(self): point = self.getPoint_generic() point.name = None self.assertEqual( point.name, None ) def test_set_name_invalidType(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.name = 1 # identifier def test_identifier_get_none(self): point = self.getPoint_generic() self.assertIsNone(point.identifier) def test_identifier_generated_type(self): point = self.getPoint_generic() point.getIdentifier() self.assertIsInstance(point.identifier, str) def test_identifier_consistency(self): point = self.getPoint_generic() point.getIdentifier() # get: twice to test consistency self.assertEqual(point.identifier, point.identifier) def test_identifier_cannot_set(self): # identifier is a read-only property point = self.getPoint_generic() with self.assertRaises(FontPartsError): point.identifier = "ABC" def test_getIdentifer_no_contour(self): point, _ = self.objectGenerator("point") with self.assertRaises(FontPartsError): point.getIdentifier() def test_getIdentifer_consistency(self): point = self.getPoint_generic() point.getIdentifier() self.assertEqual(point.identifier, point.getIdentifier()) # ---- # Hash # ---- def test_hash_object_self(self): point_one = self.getPoint_generic() self.assertEqual( hash(point_one), hash(point_one) ) def test_hash_object_other(self): point_one = self.getPoint_generic() point_two = self.getPoint_generic() self.assertNotEqual( hash(point_one), hash(point_two) ) def test_hash_object_self_variable_assignment(self): point_one = self.getPoint_generic() a = point_one self.assertEqual( hash(point_one), hash(a) ) def test_hash_object_other_variable_assignment(self): point_one = self.getPoint_generic() point_two = self.getPoint_generic() a = point_one self.assertNotEqual( hash(point_two), hash(a) ) def test_is_hashable(self): point_one = self.getPoint_generic() self.assertTrue( isinstance(point_one, collections.abc.Hashable) ) # -------- # Equality # -------- def test_object_equal_self(self): point_one = self.getPoint_generic() self.assertEqual( point_one, point_one ) def test_object_not_equal_other(self): point_one = self.getPoint_generic() point_two = self.getPoint_generic() self.assertNotEqual( point_one, point_two ) def test_object_equal_self_variable_assignment(self): point_one = self.getPoint_generic() a = point_one self.assertEqual( point_one, a ) def test_object_not_equal_other_variable_assignment(self): point_one = self.getPoint_generic() point_two = self.getPoint_generic() a = point_one self.assertNotEqual( point_two, a ) # --------- # Selection # --------- def test_selected_true(self): point = self.getPoint_generic() try: point.selected = False except NotImplementedError: return point.selected = True self.assertEqual( point.selected, True ) def test_selected_false(self): point = self.getPoint_generic() try: point.selected = False except NotImplementedError: return self.assertEqual( point.selected, False ) # ---- # Copy # ---- def test_copy_seperate_objects(self): point = self.getPoint_generic() copied = point.copy() self.assertIsNot( point, copied ) def test_copy_different_contour(self): point = self.getPoint_generic() copied = point.copy() self.assertIsNot( point.contour, copied.contour ) def test_copy_none_contour(self): point = self.getPoint_generic() copied = point.copy() self.assertEqual( copied.contour, None ) def test_copy_same_type(self): point = self.getPoint_generic() copied = point.copy() self.assertEqual( point.type, copied.type ) def test_copy_same_smooth(self): point = self.getPoint_generic() copied = point.copy() self.assertEqual( point.smooth, copied.smooth ) def test_copy_same_x(self): point = self.getPoint_generic() copied = point.copy() self.assertEqual( point.x, copied.x ) def test_copy_same_y(self): point = self.getPoint_generic() copied = point.copy() self.assertEqual( point.y, copied.y ) def test_copy_same_name(self): point = self.getPoint_generic() copied = point.copy() self.assertEqual( point.name, copied.name ) def test_copy_same_identifier_None(self): point = self.getPoint_generic() point._setIdentifier(None) copied = point.copy() self.assertEqual( point.identifier, copied.identifier, ) def test_copy_different_identifier(self): point = self.getPoint_generic() point.getIdentifier() copied = point.copy() self.assertNotEqual( point.identifier, copied.identifier, ) def test_copy_generated_identifier_different(self): otherContour, _ = self.objectGenerator("contour") point = self.getPoint_generic() copied = point.copy() copied.contour = otherContour point.getIdentifier() copied.getIdentifier() self.assertNotEqual( point.identifier, copied.identifier ) def test_copyData_type(self): point = self.getPoint_generic() pointOther, _ = self.objectGenerator("point") pointOther.copyData(point) self.assertEqual( point.type, pointOther.type, ) def test_copyData_smooth(self): point = self.getPoint_generic() pointOther, _ = self.objectGenerator("point") pointOther.copyData(point) self.assertEqual( point.smooth, pointOther.smooth, ) def test_copyData_x(self): point = self.getPoint_generic() pointOther, _ = self.objectGenerator("point") pointOther.copyData(point) self.assertEqual( point.x, pointOther.x, ) def test_copyData_y(self): point = self.getPoint_generic() pointOther, _ = self.objectGenerator("point") pointOther.copyData(point) self.assertEqual( point.y, pointOther.y, ) def test_copyData_name(self): point = self.getPoint_generic() point.name = "P" pointOther, _ = self.objectGenerator("point") pointOther.copyData(point) self.assertEqual( point.name, pointOther.name, ) def test_copyData_different_identifier(self): point = self.getPoint_generic() point.getIdentifier() pointOther, _ = self.objectGenerator("point") pointOther.copyData(point) self.assertNotEqual( point.identifier, pointOther.identifier, ) # -------------- # Transformation # -------------- def test_transformBy_valid_no_origin(self): point = self.getPoint_generic() point.transformBy((2, 0, 0, 3, -3, 2)) self.assertEqual( (point.x, point.y), (199.0, 608.0) ) def test_transformBy_valid_origin(self): point = self.getPoint_generic() point.transformBy((2, 0, 0, 2, 0, 0), origin=(1, 2)) self.assertEqual( (point.x, point.y), (201.0, 402.0) ) def test_transformBy_invalid_one_string_value(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.transformBy((1, 0, 0, 1, 0, "0")) def test_transformBy_invalid_all_string_values(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.transformBy("1, 0, 0, 1, 0, 0") def test_transformBy_invalid_int_value(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.transformBy(123) # moveBy def test_moveBy_valid(self): point = self.getPoint_generic() point.moveBy((-1, 2)) self.assertEqual( (point.x, point.y), (100.0, 204.0) ) def test_moveBy_invalid_one_string_value(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.moveBy((-1, "2")) def test_moveBy_invalid_all_strings_value(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.moveBy("-1, 2") def test_moveBy_invalid_int_value(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.moveBy(1) # scaleBy def test_scaleBy_valid_one_value_no_origin(self): point = self.getPoint_generic() point.scaleBy((-2)) self.assertEqual( (point.x, point.y), (-202.0, -404.0) ) def test_scaleBy_valid_two_values_no_origin(self): point = self.getPoint_generic() point.scaleBy((-2, 3)) self.assertEqual( (point.x, point.y), (-202.0, 606.0) ) def test_scaleBy_valid_two_values_origin(self): point = self.getPoint_generic() point.scaleBy((-2, 3), origin=(1, 2)) self.assertEqual( (point.x, point.y), (-199.0, 602.0) ) def test_scaleBy_invalid_one_string_value(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.scaleBy((-1, "2")) def test_scaleBy_invalid_two_string_values(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.scaleBy("-1, 2") def test_scaleBy_invalid_tuple_too_many_values(self): point = self.getPoint_generic() with self.assertRaises(ValueError): point.scaleBy((-1, 2, -3)) # rotateBy def test_rotateBy_valid_no_origin(self): point = self.getPoint_generic() point.rotateBy(45) self.assertEqual( [(round(point.x, 3)), (round(point.y, 3))], [-71.418, 214.253] ) def test_rotateBy_valid_origin(self): point = self.getPoint_generic() point.rotateBy(45, origin=(1, 2)) self.assertEqual( [(round(point.x, 3)), (round(point.y, 3))], [-69.711, 214.132] ) def test_rotateBy_invalid_string_value(self): point = self.getPoint_generic() with self.assertRaises(TypeError): point.rotateBy("45") def test_rotateBy_invalid_too_large_value_positive(self): point = self.getPoint_generic() with self.assertRaises(ValueError): point.rotateBy(361) def test_rotateBy_invalid_too_large_value_negative(self): point = self.getPoint_generic() with self.assertRaises(ValueError): point.rotateBy(-361) # skewBy def test_skewBy_valid_no_origin_one_value(self): point = self.getPoint_generic() point.skewBy(100) self.assertEqual( [(round(point.x, 3)), (round(point.y, 3))], [-1044.599, 202.0] ) def test_skewBy_valid_no_origin_two_values(self): point = self.getPoint_generic() point.skewBy((100, 200)) self.assertEqual( [(round(point.x, 3)), (round(point.y, 3))], [-1044.599, 238.761] ) def test_skewBy_valid_origin_one_value(self): point = self.getPoint_generic() point.skewBy(100, origin=(1, 2)) self.assertEqual( [(round(point.x, 3)), (round(point.y, 3))], [-1033.256, 202.0] ) def test_skewBy_valid_origin_two_values(self): point = self.getPoint_generic() point.skewBy((100, 200), origin=(1, 2)) self.assertEqual( [(round(point.x, 3)), (round(point.y, 3))], [-1033.256, 238.397] ) # ------------- # Normalization # ------------- # round def getPoint_floatXY(self): point, _ = self.objectGenerator("point") point.x = 101.3 point.y = 202.6 return point def test_roundX(self): point = self.getPoint_floatXY() point.round() self.assertEqual( point.x, 101 ) def test_roundY(self): point = self.getPoint_floatXY() point.round() self.assertEqual( point.y, 203 ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_segment.py000066400000000000000000000135771477533125200247030ustar00rootroot00000000000000import unittest import collections class TestSegment(unittest.TestCase): def getSegment_line(self): contour, unrequested = self.objectGenerator("contour") unrequested.append(contour) contour.appendPoint((0, 0), "move") contour.appendPoint((101, 202), "line") segment = contour[1] return segment # ---- # Type # ---- def test_type_get(self): segment = self.getSegment_line() self.assertEqual( segment.type, "line" ) def test_set_move(self): segment = self.getSegment_line() segment.type = "move" self.assertEqual( segment.type, "move" ) def test_len_move(self): segment = self.getSegment_line() segment.type = "move" self.assertEqual( len(segment.points), 1 ) def test_oncuve_type_move(self): segment = self.getSegment_line() segment.type = "move" self.assertEqual( segment.onCurve.type, "move" ) def test_oncuve_x_y(self): segment = self.getSegment_line() segment.type = "move" self.assertEqual( (segment.onCurve.x, segment.onCurve.y), (101, 202) ) def test_set_curve(self): segment = self.getSegment_line() segment.type = "curve" self.assertEqual( segment.type, "curve" ) def test_len_curve(self): segment = self.getSegment_line() segment.type = "curve" self.assertEqual( len(segment.points), 3 ) def test_curve_pt_types(self): segment = self.getSegment_line() segment.type = "curve" types = tuple(point.type for point in segment.points) self.assertEqual( types, ("offcurve", "offcurve", "curve") ) def test_curve_pt_x_y(self): segment = self.getSegment_line() segment.type = "curve" coordinates = tuple((point.x, point.y) for point in segment.points) self.assertEqual( coordinates, ((0, 0), (101, 202), (101, 202)) ) def test_set_qcurve(self): segment = self.getSegment_line() segment.type = "qcurve" self.assertEqual( segment.type, "qcurve" ) def test_len_qcurve(self): segment = self.getSegment_line() segment.type = "qcurve" self.assertEqual( len(segment.points), 3 ) def test_qcurve_pt_types(self): segment = self.getSegment_line() segment.type = "qcurve" types = tuple(point.type for point in segment.points) self.assertEqual( types, ("offcurve", "offcurve", "qcurve") ) def test_qcurve_pt_x_y(self): segment = self.getSegment_line() segment.type = "qcurve" coordinates = tuple((point.x, point.y) for point in segment.points) self.assertEqual( coordinates, ((0, 0), (101, 202), (101, 202)) ) def test_set_invalid_segment_type_string(self): segment = self.getSegment_line() with self.assertRaises(ValueError): segment.type = "xxx" def test_set_invalid_segment_type_int(self): segment = self.getSegment_line() with self.assertRaises(TypeError): segment.type = 123 def test_offCurve_only_segment(self): contour, unrequested = self.objectGenerator("contour") unrequested.append(contour) contour.appendPoint((0, 0), "offcurve") contour.appendPoint((100, 0), "offcurve") contour.appendPoint((100, 100), "offcurve") contour.appendPoint((0, 100), "offcurve") segment = contour[0] self.assertEqual( len(contour), 1 ) # onCurve is a dummy None value, telling this is an on-curve-less quad blob self.assertIsNone( segment.onCurve, ) self.assertEqual( segment.points, segment.offCurve ) self.assertEqual( segment.type, "qcurve" ) # ---- # Hash # ---- def test_hash(self): segment = self.getSegment_line() self.assertEqual( isinstance(segment, collections.abc.Hashable), False ) # -------- # Equality # -------- def test_object_equal_self(self): segment_one = self.getSegment_line() self.assertEqual( segment_one, segment_one ) def test_object_not_equal_other(self): segment_one = self.getSegment_line() segment_two = self.getSegment_line() self.assertNotEqual( segment_one, segment_two ) def test_object_equal_self_variable_assignment(self): segment_one = self.getSegment_line() a = segment_one self.assertEqual( segment_one, a ) def test_object_not_equal_other_variable_assignment(self): segment_one = self.getSegment_line() segment_two = self.getSegment_line() a = segment_one self.assertNotEqual( segment_two, a ) # --------- # Selection # --------- def test_selected_true(self): segment = self.getSegment_line() try: segment.selected = False except NotImplementedError: return segment.selected = True self.assertEqual( segment.selected, True ) def test_selected_false(self): segment = self.getSegment_line() try: segment.selected = False except NotImplementedError: return self.assertEqual( segment.selected, False ) robotools-fontParts-26e8b8c/Lib/fontParts/test/test_world.py000066400000000000000000000231351477533125200243570ustar00rootroot00000000000000import unittest import tempfile import os from fontParts.world import RFont, FontList, OpenFont, OpenFonts class TestFontList(unittest.TestCase): def getFont(self): font, _ = self.objectGenerator("font") return font # ---- # Sort # ---- def getFonts_sortBy(self, attr, values): fonts = FontList() for value in values: font = self.getFont() setattr(font.info, attr, value) if attr != "familyName": font.info.familyName = "%s %s" % (attr, repr(value)) fonts.append(font) return fonts # Sort Description Strings # ------------------------ def getFont_sortBy_monospaceGlyphs(self): font = self.getFont() font.info.familyName = "monospace %s" % str(id(font)) glyph1 = font.newGlyph("a") glyph1.width = 100 glyph2 = font.newGlyph("b") glyph2.width = 100 return font def getFont_sortBy_proportionalGlyphs(self): font = self.getFont() font.info.familyName = "proportional %s" % str(id(font)) glyph1 = font.newGlyph("a") glyph1.width = 100 glyph2 = font.newGlyph("b") glyph2.width = 200 return font # familyName def test_sortBy_familyName(self): fonts = self.getFonts_sortBy( "familyName", ["aaa", "bbb", "ccc", None] ) font1, font2, font3, font4 = fonts fonts.sortBy("familyName") expected = [font4, font1, font2, font3] self.assertEqual(fonts, expected) # styleName def test_sortBy_styleName(self): fonts = self.getFonts_sortBy( "styleName", ["aaa", "bbb", "ccc", None] ) font1, font2, font3, font4 = fonts fonts.sortBy("styleName") expected = [font4, font1, font2, font3] self.assertEqual(fonts, expected) # isRoman def test_sortBy_isRoman_styleMapStyleName(self): fonts = self.getFonts_sortBy( "styleMapStyleName", ["regular", "italic", "bold", "bold italic"] ) font1, font2, font3, font4 = fonts fonts.reverse() fonts.sortBy("isRoman") expected = [font3, font1, font4, font2] self.assertEqual(fonts, expected) def test_sortBy_isRoman_italicAngle(self): fonts = self.getFonts_sortBy( "italicAngle", [1, 2, 3, 0] ) font1, font2, font3, font4 = fonts fonts.sortBy("isRoman") expected = [font4, font1, font2, font3] self.assertEqual(fonts, expected) # isItalic def test_sortBy_isItalic_styleMapStyleName(self): fonts = self.getFonts_sortBy( "styleMapStyleName", ["regular", "italic", "bold", "bold italic"] ) font1, font2, font3, font4 = fonts fonts.sortBy("isItalic") expected = [font2, font4, font1, font3] self.assertEqual(fonts, expected) def test_sortBy_isItalic_italicAngle(self): fonts = self.getFonts_sortBy( "italicAngle", [0, 1, 2, 3] ) font1, font2, font3, font4 = fonts fonts.sortBy("isItalic") expected = [font2, font3, font4, font1] self.assertEqual(fonts, expected) # widthValue def test_sortBy_widthValue(self): fonts = self.getFonts_sortBy( "openTypeOS2WidthClass", [1, 2, 3, None] ) font1, font2, font3, font4 = fonts fonts.sortBy("widthValue") expected = [font4, font1, font2, font3] self.assertEqual(fonts, expected) # weightValue def test_sortBy_weightValue(self): fonts = self.getFonts_sortBy( "openTypeOS2WeightClass", [100, 200, 300, None] ) font1, font2, font3, font4 = fonts fonts.sortBy("weightValue") expected = [font4, font1, font2, font3] self.assertEqual(fonts, expected) # isMonospace def test_sortBy_isMonospace_postscriptIsFixedPitch(self): fonts = self.getFonts_sortBy( "postscriptIsFixedPitch", [True, True, False, False] ) font1, font2, font3, font4 = fonts fonts.reverse() fonts.sortBy("isMonospace") expected = [font2, font1, font4, font3] self.assertEqual(fonts, expected) def test_sortBy_isMonospace_glyphs(self): font1 = self.getFont_sortBy_monospaceGlyphs() font2 = self.getFont_sortBy_monospaceGlyphs() font3 = self.getFont_sortBy_proportionalGlyphs() font4 = self.getFont_sortBy_proportionalGlyphs() fonts = FontList() fonts.extend([font1, font2, font3, font4]) fonts.reverse() fonts.sortBy("isMonospace") expected = [font2, font1, font4, font3] self.assertEqual(fonts, expected) # isProportional def test_sortBy_isProportional_postscriptIsFixedPitch(self): fonts = self.getFonts_sortBy( "postscriptIsFixedPitch", [False, False, True, True] ) font1, font2, font3, font4 = fonts fonts.reverse() fonts.sortBy("isProportional") expected = [font2, font1, font4, font3] self.assertEqual(fonts, expected) def test_sortBy_isProportional_glyphs(self): font1 = self.getFont_sortBy_monospaceGlyphs() font2 = self.getFont_sortBy_monospaceGlyphs() font3 = self.getFont_sortBy_proportionalGlyphs() font4 = self.getFont_sortBy_proportionalGlyphs() fonts = FontList() fonts.extend([font1, font2, font3, font4]) fonts.sortBy("isProportional") expected = [font3, font4, font1, font2] self.assertEqual(fonts, expected) # font.info Attributes def test_sortBy_fontInfoAttribute_xHeight(self): fonts = self.getFonts_sortBy( "xHeight", [10, 20, 30, 40] ) font1, font2, font3, font4 = fonts fonts.reverse() fonts.sortBy("xHeight") expected = [font1, font2, font3, font4] self.assertEqual(fonts, expected) # Sort Value Function # ------------------- def getFont_withGlyphCount(self, count): font = self.getFont() for i in range(count): font.newGlyph("glyph%d" % i) font.info.familyName = str(count) return font def test_sortBy_sortValueFunction(self): font1 = self.getFont_withGlyphCount(10) font2 = self.getFont_withGlyphCount(20) font3 = self.getFont_withGlyphCount(30) font4 = self.getFont_withGlyphCount(40) fonts = FontList() fonts.extend([font1, font2, font3, font4]) fonts.reverse() def glyphCountSortValue(font): return len(font) fonts.sortBy(glyphCountSortValue) expected = [font1, font2, font3, font4] self.assertEqual(fonts, expected) # ------ # Search # ------ # family name def test_getFontsByFamilyName(self): font1 = self.getFont() font1.info.familyName = "A" font2 = self.getFont() font2.info.familyName = "B" font3 = self.getFont() font3.info.familyName = "C" font4 = self.getFont() font4.info.familyName = "A" fonts = FontList() fonts.extend([font1, font2, font3, font4]) found = fonts.getFontsByFamilyName("A") self.assertEqual(found, [font1, font4]) # style name def test_getFontsByStyleName(self): font1 = self.getFont() font1.info.styleName = "A" font2 = self.getFont() font2.info.styleName = "B" font3 = self.getFont() font3.info.styleName = "C" font4 = self.getFont() font4.info.styleName = "A" fonts = FontList() fonts.extend([font1, font2, font3, font4]) found = fonts.getFontsByStyleName("A") self.assertEqual(found, [font1, font4]) # family name, style name def test_getFontsByFamilyNameStyleName(self): font1 = self.getFont() font1.info.familyName = "A" font1.info.styleName = "1" font2 = self.getFont() font2.info.familyName = "A" font2.info.styleName = "2" font3 = self.getFont() font3.info.familyName = "B" font3.info.styleName = "1" font4 = self.getFont() font4.info.familyName = "A" font4.info.styleName = "1" fonts = FontList() fonts.extend([font1, font2, font3, font4]) found = fonts.getFontsByFamilyNameStyleName("A", "1") self.assertEqual(found, [font1, font4]) class TestFontOpen(unittest.TestCase): def setUp(self): font, _ = self.objectGenerator("font") self.font_dir = tempfile.mkdtemp() self.font_path = os.path.join(self.font_dir, "test.ufo") font.save(self.font_path) def tearDown(self): import shutil shutil.rmtree(self.font_dir) def test_font_open(self): OpenFont(self.font_path) class TestOpenFonts(unittest.TestCase): def setUp(self): self.font_dir = tempfile.mkdtemp() for i in range(3): font, _ = self.objectGenerator("font") path = os.path.join(self.font_dir, f"test{i}.ufo") font.save(path) def tearDown(self): import shutil shutil.rmtree(self.font_dir) def test_font_open(self): fonts = OpenFonts(self.font_dir) fileNames = [os.path.basename(font.path) for font in fonts] fileNames.sort() expected = ["test0.ufo", "test1.ufo", "test2.ufo"] self.assertEqual(fileNames, expected) class TestFontShell_RFont(unittest.TestCase): def test_fontshell_RFont_empty(self): RFont() robotools-fontParts-26e8b8c/Lib/fontParts/ui.py000066400000000000000000000124011477533125200216210ustar00rootroot00000000000000from fontParts.world import _EnvironmentDispatcher def AskString(message, value='', title='FontParts'): """ An ask a string dialog, a `message` is required. Optionally a `value` and `title` can be provided. :: from fontParts.ui import AskString print(AskString("who are you?")) """ return dispatcher["AskString"](message=message, value=value, title=title) def AskYesNoCancel(message, title='FontParts', default=0, informativeText=""): """ An ask yes, no or cancel dialog, a `message` is required. Optionally a `title`, `default` and `informativeText` can be provided. The `default` option is to indicate which button is the default button. :: from fontParts.ui import AskYesNoCancel print(AskYesNoCancel("who are you?")) """ return dispatcher["AskYesNoCancel"](message=message, title=title, default=default, informativeText=informativeText) def FindGlyph(aFont, message="Search for a glyph:", title='FontParts'): """ A dialog to search a glyph for a provided font. Optionally a `message`, `title` and `allFonts` can be provided. from fontParts.ui import FindGlyph from fontParts.world import CurrentFont glyph = FindGlyph(CurrentFont()) print(glyph) """ return dispatcher["FindGlyph"](aFont=aFont, message=message, title=title) def GetFile(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None): """ An get file dialog. Optionally a `message`, `title`, `directory`, `fileName` and `allowsMultipleSelection` can be provided. :: from fontParts.ui import GetFile print(GetFile()) """ return dispatcher["GetFile"](message=message, title=title, directory=directory, fileName=fileName, allowsMultipleSelection=allowsMultipleSelection, fileTypes=fileTypes) def GetFileOrFolder(message=None, title=None, directory=None, fileName=None, allowsMultipleSelection=False, fileTypes=None): """ An get file or folder dialog. Optionally a `message`, `title`, `directory`, `fileName`, `allowsMultipleSelection` and `fileTypes` can be provided. :: from fontParts.ui import GetFileOrFolder print(GetFileOrFolder()) """ return dispatcher["GetFileOrFolder"](message=message, title=title, directory=directory, fileName=fileName, allowsMultipleSelection=allowsMultipleSelection, fileTypes=fileTypes) def Message(message, title='FontParts', informativeText=""): """ An message dialog. Optionally a `message`, `title` and `informativeText` can be provided. :: from fontParts.ui import Message print(Message("This is a message")) """ return dispatcher["Message"](message=message, title=title, informativeText=informativeText) def PutFile(message=None, fileName=None): """ An put file dialog. Optionally a `message` and `fileName` can be provided. :: from fontParts.ui import PutFile print(PutFile()) """ return dispatcher["PutFile"](message=message, fileName=fileName) def SearchList(items, message="Select an item:", title='FontParts'): """ A dialgo to search a given list. Optionally a `message`, `title` and `allFonts` can be provided. :: from fontParts.ui import SearchList result = SearchList(["a", "b", "c"]) print(result) """ return dispatcher["SearchList"](items=items, message=message, title=title) def SelectFont(message="Select a font:", title='FontParts', allFonts=None): """ Select a font from all open fonts. Optionally a `message`, `title` and `allFonts` can be provided. If `allFonts` is `None` it will list all open fonts. :: from fontParts.ui import SelectFont font = SelectFont() print(font) """ return dispatcher["SelectFont"](message=message, title=title, allFonts=allFonts) def SelectGlyph(aFont, message="Select a glyph:", title='FontParts'): """ Select a glyph for a given font. Optionally a `message` and `title` can be provided. :: from fontParts.ui import SelectGlyph font = CurrentFont() glyph = SelectGlyph(font) print(glyph) """ return dispatcher["SelectGlyph"](aFont=aFont, message=message, title=title) def ProgressBar(title="RoboFab...", ticks=None, label=""): """ A progess bar dialog. Optionally a `title`, `ticks` and `label` can be provided. :: from fontParts.ui import ProgressBar bar = ProgressBar() # do something bar.close() """ return dispatcher["ProgressBar"](title=title, ticks=ticks, label=label) # ---------- # Dispatcher # ---------- dispatcher = _EnvironmentDispatcher([ "AskString", "AskYesNoCancel", "FindGlyph", "GetFile", "GetFolder", "GetFileOrFolder", "Message", "OneList", "PutFile", "SearchList", "SelectFont", "SelectGlyph", "ProgressBar", ]) robotools-fontParts-26e8b8c/Lib/fontParts/world.py000066400000000000000000000464711477533125200223510ustar00rootroot00000000000000import os import glob def OpenFonts(directory=None, showInterface=True, fileExtensions=None): """ Open all fonts with the given **fileExtensions** located in **directory**. If **directory** is ``None``, a dialog for selecting a directory will be opened. **directory** may also be a list of directories. If **showInterface** is ``False``, the font should be opened without graphical interface. The default for **showInterface** is ``True``. The fonts are located within the directory using the `glob` `_ module. The patterns are created with ``os.path.join(glob, "*" + fileExtension)`` for every file extension in ``fileExtensions``. If ``fileExtensions`` if ``None`` the environment will use its default fileExtensions. :: from fontParts.world import * fonts = OpenFonts() fonts = OpenFonts(showInterface=False) """ from fontParts.ui import GetFileOrFolder if fileExtensions is None: fileExtensions = dispatcher["OpenFontsFileExtensions"] if isinstance(directory, str): directories = [directory] elif directory is None: directories = GetFileOrFolder(allowsMultipleSelection=True) else: directories = directory if directories: globPatterns = [] for directory in directories: if os.path.splitext(directory)[-1] in fileExtensions: globPatterns.append(directory) elif not os.path.isdir(directory): pass else: for ext in fileExtensions: globPatterns.append(os.path.join(directory, "*" + ext)) paths = [] for pattern in globPatterns: paths.extend(glob.glob(pattern)) for path in paths: yield OpenFont(path, showInterface=showInterface) def OpenFont(path, showInterface=True): """ Open font located at **path**. If **showInterface** is ``False``, the font should be opened without graphical interface. The default for **showInterface** is ``True``. :: from fontParts.world import * font = OpenFont("/path/to/my/font.ufo") font = OpenFont("/path/to/my/font.ufo", showInterface=False) """ return dispatcher["OpenFont"](pathOrObject=path, showInterface=showInterface) def NewFont(familyName=None, styleName=None, showInterface=True): """ Create a new font. **familyName** will be assigned to ``font.info.familyName`` and **styleName** will be assigned to ``font.info.styleName``. These are optional and default to ``None``. If **showInterface** is ``False``, the font should be created without graphical interface. The default for **showInterface** is ``True``. :: from fontParts.world import * font = NewFont() font = NewFont(familyName="My Family", styleName="My Style") font = NewFont(showInterface=False) """ return dispatcher["NewFont"](familyName=familyName, styleName=styleName, showInterface=showInterface) def CurrentFont(): """ Get the "current" font. """ return dispatcher["CurrentFont"]() def CurrentGlyph(): """ Get the "current" glyph from :func:`CurrentFont`. :: from fontParts.world import * glyph = CurrentGlyph() """ return dispatcher["CurrentGlyph"]() def CurrentLayer(): """ Get the "current" layer from :func:`CurrentGlyph`. :: from fontParts.world import * layer = CurrentLayer() """ return dispatcher["CurrentLayer"]() def CurrentContours(): """ Get the "currently" selected contours from :func:`CurrentGlyph`. :: from fontParts.world import * contours = CurrentContours() This returns an immutable list, even when nothing is selected. """ return dispatcher["CurrentContours"]() def _defaultCurrentContours(): glyph = CurrentGlyph() if glyph is None: return () return glyph.selectedContours def CurrentSegments(): """ Get the "currently" selected segments from :func:`CurrentContours`. :: from fontParts.world import * segments = CurrentSegments() This returns an immutable list, even when nothing is selected. """ return dispatcher["CurrentSegments"]() def _defaultCurrentSegments(): glyph = CurrentGlyph() if glyph is None: return () segments = [] for contour in glyph.selectedContours: segments.extend(contour.selectedSegments) return tuple(segments) def CurrentPoints(): """ Get the "currently" selected points from :func:`CurrentContours`. :: from fontParts.world import * points = CurrentPoints() This returns an immutable list, even when nothing is selected. """ return dispatcher["CurrentPoints"]() def _defaultCurrentPoints(): glyph = CurrentGlyph() if glyph is None: return () points = [] for contour in glyph.selectedContours: points.extend(contour.selectedPoints) return tuple(points) def CurrentComponents(): """ Get the "currently" selected components from :func:`CurrentGlyph`. :: from fontParts.world import * components = CurrentComponents() This returns an immutable list, even when nothing is selected. """ return dispatcher["CurrentComponents"]() def _defaultCurrentComponents(): glyph = CurrentGlyph() if glyph is None: return () return glyph.selectedComponents def CurrentAnchors(): """ Get the "currently" selected anchors from :func:`CurrentGlyph`. :: from fontParts.world import * anchors = CurrentAnchors() This returns an immutable list, even when nothing is selected. """ return dispatcher["CurrentAnchors"]() def _defaultCurrentAnchors(): glyph = CurrentGlyph() if glyph is None: return () return glyph.selectedAnchors def CurrentGuidelines(): """ Get the "currently" selected guidelines from :func:`CurrentGlyph`. This will include both font level and glyph level guidelines. :: from fontParts.world import * guidelines = CurrentGuidelines() This returns an immutable list, even when nothing is selected. """ return dispatcher["CurrentGuidelines"]() def _defaultCurrentGuidelines(): guidelines = [] font = CurrentFont() if font is not None: guidelines.extend(font.selectedGuidelines) glyph = CurrentGlyph() if glyph is not None: guidelines.extend(glyph.selectedGuidelines) return tuple(guidelines) def AllFonts(sortOptions=None): """ Get a list of all open fonts. Optionally, provide a value for ``sortOptions`` to sort the fonts. See :meth:`world.FontList.sortBy` for options. :: from fontParts.world import * fonts = AllFonts() for font in fonts: # do something fonts = AllFonts("magic") for font in fonts: # do something fonts = AllFonts(["familyName", "styleName"]) for font in fonts: # do something """ fontList = FontList(dispatcher["AllFonts"]()) if sortOptions is not None: fontList.sortBy(sortOptions) return fontList def RFont(path=None, showInterface=True): return dispatcher["RFont"](pathOrObject=path, showInterface=showInterface) def RGlyph(): return dispatcher["RGlyph"]() # --------- # Font List # --------- def FontList(fonts=None): """ Get a list with font specific methods. :: from fontParts.world import * fonts = FontList() Refer to :class:`BaseFontList` for full documentation. """ l = dispatcher["FontList"]() if fonts: l.extend(fonts) return l class BaseFontList(list): # Sort def sortBy(self, sortOptions, reverse=False): """ Sort ``fonts`` with the ordering preferences defined by ``sortBy``. ``sortBy`` must be one of the following: * sort description string * :class:`BaseInfo` attribute name * sort value function * list/tuple containing sort description strings, :class:`BaseInfo` attribute names and/or sort value functions * ``"magic"`` Sort Description Strings ------------------------ The sort description strings, and how they modify the sort, are: +----------------------+--------------------------------------+ | ``"familyName"`` | Family names by alphabetical order. | +----------------------+--------------------------------------+ | ``"styleName"`` | Style names by alphabetical order. | +----------------------+--------------------------------------+ | ``"isItalic"`` | Italics before romans. | +----------------------+--------------------------------------+ | ``"isRoman"`` | Romans before italics. | +----------------------+--------------------------------------+ | ``"widthValue"`` | Width values by numerical order. | +----------------------+--------------------------------------+ | ``"weightValue"`` | Weight values by numerical order. | +----------------------+--------------------------------------+ | ``"monospace"`` | Monospaced before proportional. | +----------------------+--------------------------------------+ | ``"isProportional"`` | Proportional before monospaced. | +----------------------+--------------------------------------+ :: >>> fonts.sortBy(("familyName", "styleName")) Font Info Attribute Names ------------------------- Any :class:`BaseFont` attribute name may be included as a sort option. For example, to sort by x-height value, you'd use the ``"xHeight"`` attribute name. :: >>> fonts.sortBy("xHeight") Sort Value Function ------------------- A sort value function must be a function that accepts one argument, ``font``. This function must return a sortable value for the given font. For example: :: >>> def glyphCountSortValue(font): >>> return len(font) >>> >>> fonts.sortBy(glyphCountSortValue) A list of sort description strings and/or sort functions may also be provided. This should be in order of most to least important. For example, to sort by family name and then style name, do this: "magic" ------- If "magic" is given for ``sortBy``, the fonts will be sorted based on this sort description sequence: * ``"familyName"`` * ``"isProportional"`` * ``"widthValue"`` * ``"weightValue"`` * ``"styleName"`` * ``"isRoman"`` :: >>> fonts.sortBy("magic") """ from types import FunctionType valueGetters = dict( familyName=_sortValue_familyName, styleName=_sortValue_styleName, isRoman=_sortValue_isRoman, isItalic=_sortValue_isItalic, widthValue=_sortValue_widthValue, weightValue=_sortValue_weightValue, isProportional=_sortValue_isProportional, isMonospace=_sortValue_isMonospace ) if isinstance(sortOptions, str) or isinstance(sortOptions, FunctionType): sortOptions = [sortOptions] if not isinstance(sortOptions, (list, tuple)): raise ValueError("sortOptions must a string, list or function.") if not sortOptions: raise ValueError("At least one sort option must be defined.") if sortOptions == ["magic"]: sortOptions = [ "familyName", "isProportional", "widthValue", "weightValue", "styleName", "isRoman" ] sorter = [] for originalIndex, font in enumerate(self): sortable = [] for valueName in sortOptions: if isinstance(valueName, FunctionType): value = valueName(font) elif valueName in valueGetters: value = valueGetters[valueName](font) elif hasattr(font.info, valueName): value = getattr(font.info, valueName) else: raise ValueError("Unknown sort option: %s" % repr(valueName)) sortable.append(value) sortable.append(originalIndex) sortable.append(font) sorter.append(tuple(sortable)) sorter.sort() fonts = [i[-1] for i in sorter] del self[:] self.extend(fonts) if reverse: self.reverse() # Search def getFontsByFontInfoAttribute(self, *attributeValuePairs): """ Get a list of fonts that match the (attribute, value) combinations in ``attributeValuePairs``. :: >>> subFonts = fonts.getFontsByFontInfoAttribute(("xHeight", 20)) >>> subFonts = fonts.getFontsByFontInfoAttribute(("xHeight", 20), ("descender", -150)) This will return an instance of :class:`BaseFontList`. """ found = self for attr, value in attributeValuePairs: found = self._matchFontInfoAttributes(found, (attr, value)) return found def _matchFontInfoAttributes(self, fonts, attributeValuePair): found = self.__class__() attr, value = attributeValuePair for font in fonts: if getattr(font.info, attr) == value: found.append(font) return found def getFontsByFamilyName(self, familyName): """ Get a list of fonts that match ``familyName``. This will return an instance of :class:`BaseFontList`. """ return self.getFontsByFontInfoAttribute(("familyName", familyName)) def getFontsByStyleName(self, styleName): """ Get a list of fonts that match ``styleName``. This will return an instance of :class:`BaseFontList`. """ return self.getFontsByFontInfoAttribute(("styleName", styleName)) def getFontsByFamilyNameStyleName(self, familyName, styleName): """ Get a list of fonts that match ``familyName`` and ``styleName``. This will return an instance of :class:`BaseFontList`. """ return self.getFontsByFontInfoAttribute(("familyName", familyName), ("styleName", styleName)) def _sortValue_familyName(font): """ Returns font.info.familyName. """ value = font.info.familyName if value is None: value = "" return value def _sortValue_styleName(font): """ Returns font.info.styleName. """ value = font.info.styleName if value is None: value = "" return value def _sortValue_isRoman(font): """ Returns 0 if the font is roman. Returns 1 if the font is not roman. """ italic = _sortValue_isItalic(font) if italic == 1: return 0 return 1 def _sortValue_isItalic(font): """ Returns 0 if the font is italic. Returns 1 if the font is not italic. """ info = font.info styleMapStyleName = info.styleMapStyleName if styleMapStyleName is not None and "italic" in styleMapStyleName: return 0 if info.italicAngle not in (None, 0): return 0 return 1 def _sortValue_widthValue(font): """ Returns font.info.openTypeOS2WidthClass. """ value = font.info.openTypeOS2WidthClass if value is None: value = -1 return value def _sortValue_weightValue(font): """ Returns font.info.openTypeOS2WeightClass. """ value = font.info.openTypeOS2WeightClass if value is None: value = -1 return value def _sortValue_isProportional(font): """ Returns 0 if the font is proportional. Returns 1 if the font is not proportional. """ monospace = _sortValue_isMonospace(font) if monospace == 1: return 0 return 1 def _sortValue_isMonospace(font): """ Returns 0 if the font is monospace. Returns 1 if the font is not monospace. """ if font.info.postscriptIsFixedPitch: return 0 if not len(font): return 1 testWidth = None for glyph in font: if testWidth is None: testWidth = glyph.width else: if testWidth != glyph.width: return 1 return 0 # ---------- # Dispatcher # ---------- class _EnvironmentDispatcher(object): def __init__(self, registryItems): self._registry = {item: None for item in registryItems} def __setitem__(self, name, func): self._registry[name] = func def __getitem__(self, name): func = self._registry[name] if func is None: raise NotImplementedError return func dispatcher = _EnvironmentDispatcher([ "OpenFontsFileExtensions", "OpenFont", "NewFont", "AllFonts", "CurrentFont", "CurrentGlyph", "CurrentLayer", "CurrentContours", "CurrentSegments", "CurrentPoints", "CurrentComponents", "CurrentAnchors", "CurrentGuidelines", "FontList", "RFont", "RLayer", "RGlyph", "RContour", "RPoint", "RAnchor", "RComponent", "RGuideline", "RImage", "RInfo", "RFeatures", "RGroups", "RKerning", "RLib", ]) # Register the default functions. dispatcher["CurrentContours"] = _defaultCurrentContours dispatcher["CurrentSegments"] = _defaultCurrentSegments dispatcher["CurrentPoints"] = _defaultCurrentPoints dispatcher["CurrentComponents"] = _defaultCurrentComponents dispatcher["CurrentAnchors"] = _defaultCurrentAnchors dispatcher["CurrentGuidelines"] = _defaultCurrentGuidelines dispatcher["FontList"] = BaseFontList # ------- # fontshell # ------- try: from fontParts import fontshell # OpenFonts dispatcher["OpenFontsFileExtensions"] = [".ufo"] # OpenFont, RFont def _fontshellRFont(pathOrObject=None, showInterface=True): return fontshell.RFont(pathOrObject=pathOrObject, showInterface=showInterface) dispatcher["OpenFont"] = _fontshellRFont dispatcher["RFont"] = _fontshellRFont # NewFont def _fontshellNewFont(familyName=None, styleName=None, showInterface=True): font = fontshell.RFont(showInterface=showInterface) if familyName is not None: font.info.familyName = familyName if styleName is not None: font.info.styleName = styleName return font dispatcher["NewFont"] = _fontshellNewFont # RLayer, RGlyph, RContour, RPoint, RAnchor, RComponent, RGuideline, RImage, RInfo, RFeatures, RGroups, RKerning, RLib dispatcher["RLayer"] = fontshell.RLayer dispatcher["RGlyph"] = fontshell.RGlyph dispatcher["RContour"] = fontshell.RContour dispatcher["RPoint"] = fontshell.RPoint dispatcher["RAnchor"] = fontshell.RAnchor dispatcher["RComponent"] = fontshell.RComponent dispatcher["RGuideline"] = fontshell.RGuideline dispatcher["RImage"] = fontshell.RImage dispatcher["RInfo"] = fontshell.RInfo dispatcher["RFeatures"] = fontshell.RFeatures dispatcher["RGroups"] = fontshell.RGroups dispatcher["RKerning"] = fontshell.RKerning dispatcher["RLib"] = fontshell.RLib except ImportError: pass robotools-fontParts-26e8b8c/MANIFEST.in000066400000000000000000000000621477533125200177020ustar00rootroot00000000000000include LICENSE include README.md include tox.ini robotools-fontParts-26e8b8c/NEWS.rst000066400000000000000000000234041477533125200174570ustar00rootroot000000000000000.12.2 (released 2024-08-07) --------------------------- - Replace remaining usage of assertEquals with assertEqual. See #720 - Fixes/tweaks to documentation - Get guidelines from the mathInfo object directly. See #738 0.12.1 (released 2023-10-30) --------------------------- - Tweak to logic of `setStartSegment` 0.12.0 (released 2023-10-30) --------------------------- - Fixes to `setStartSegment` so that it keeps the start point on-curve and prevents setting a setting a start segment on an open contour (issues #709 and #412). Thanks @typesupply! - Fixes to docs and test setup. Boring things. 0.11.0 (released 2022-12-09) --------------------------- - Expose the `mathGlyph` options. Thanks @typesupply! See #672 - Set defaultLayer to "public.default" when its available. Fixes issue #674. Thanks @typemytype! See #675. - Add `info.update` to the info object. Thanks @typesupply! See #676 0.10.8 (released 2022-09-03) --------------------------- - Adds `setStartPoint` to the contour object. Thanks @typemytype! See #668. 0.10.7 (released 2022-07-11) --------------------------- - Small documentation update and fix for scm tools. 0.10.6 (released 2022-06-21) --------------------------- - Adds `openFonts` (more than one font). Issue #545. (thanks @typesupply!) 0.10.5 (released 2022-05-10) --------------------------- - Adds `glyph.autoContourOrder`. Issue #645. (thanks @roberto-arista!) - Adds `FuzzyNumber` to `base.py`. Needed for the above, copied from defcon. (thanks @typemytype!) 0.10.4 (released 2022-03-17) --------------------------- - Fixes issue with setting glyph name when copying. Issue #633. (thanks @typemytype!) 0.10.3 (released 2022-02-24) --------------------------- - Fixes issue with `defaultLayer` and copying a `font`. Issue #630. (thanks @typemytype!) 0.10.2 (released 2022-01-05) --------------------------- - Add vaidate kwarg to _loadFromGlyph #623. (thanks @ctrlcctrlv) 0.10.1 (released 2021-12-28) --------------------------- - Update to use Collections.abc.Hashable by @benkiel in #622 - Start testing Python 3.10 by @benkiel in #619 0.10.0 (released 2021-12-28) --------------------------- - 2021-12-28: Drops support for Python 3.6 - 2021-12-14: Adds tempLib, #615 (thanks @typemytype!) - Improved docs with #605 and #607. Thanks @driehuis and @arrowtype! 0.9.11 (released 2021-08-06) --------------------------- - 2021-08-06: Fixes inserting a segment with an open contour, #601 (thanks @typemytype!) 0.9.10 (released 2021-03-09) --------------------------- - 2021-03-09: Update to Defcon 0.8.0 (thanks @justvanrossum!) 0.9.9 (released 2021-02-13) --------------------------- - 2021-02-13: Fixed import of version. (#573, thanks @gyscos!) 0.9.8 (released 2021-02-12) --------------------------- - 2021-02-11: Add support for quadratic curves with no on-curve points in Contour and Segment. (#572, thanks @typemytype!) 0.9.7 (released 2020-12-23) --------------------------- - 2020-12-23: Change to github actions for CI and release. - 2020-12-18: fontShell returns `None` when referenced file name doesn't exist for an `Image` (#567, thanks @typemytype) 0.9.6 (released 2020-09-06) --------------------------- - 2020-09-06: fontShell has `changed()` implemented now 0.9.5 (released 2020-09-04) --------------------------- - 2020-09-04: Fix for contours not getting updated in fontShell, thanks @justvanrossum! - 2020-09-03: Fix for error message in normalizeKerningKey, thanks @colinmford! 0.9.4 (released 2020-08-26) --------------------------- - Fixed release build 0.9.3 (released 2020-08-26) --------------------------- - 2020-07-14: All rounding uses otRound. #536, fixes #533. Thanks @colinmford! - 2019-12-23: Allow contour.segment to be empty (#480). Thanks @typemytype! - 2020-01-08: Image file names now get a png file extension (#482). Thanks @typemytype! - 2020-02-03: Fixed error in setting contour index (#488). Thanks @typemytype! - 2020-02-10: Fixed error in PointPositionMixin (#486, fixed by #491) - 2020-04-01: Added option to turn off normalizer tests - 2020-04-07: Test fixes and updates. #512. Thanks @schriftgestalt! - various: Documentation updates and corrections 0.9.2 (released 2019-12-10) --------------------------- - 2019-12-10: No longer send or recieve images from math glyphs. (thanks @letterror) - 2019-12-10: Removed unittest2 dependency. - 2019-10-21: Only use copy in _appendContour only if there is an offset. (thanks @simoncozens) - 2019-09-29: [fontshell] Accept pathLikeObjects for opening. 0.9.1 (released 2019-09-28) --------------------------- - 2019-09-28: Change how `glyph.unicode` behaves. Instead of adding to `glyph.unicodes`, on a `set` it sets `glyph.unicodes` to the single value provided (or an empty list if the value was `None`.) - 2019-09-23: Fix an error in world.py 0.9.0 (released 2019-08-30) --------------------------- This release only supports Python 3, if you need Python 2 support, use 0.8.9. - 2019-08-30: Remove Python 2 support. - 2019-08-30: Change rounding to always round to the higher number, matching what fontTools does for anything visual. 0.8.9 (released 2019-08-25) --------------------------- - 2019-08-25: Simplify `removeOverlap` in fontShell - 2019-08-25: Fixup dev-requirements Note: This will be one of the last releases to support Python2. 0.8.8 (released 2019-08-23) --------------------------- - 2019-08-23: Fix `removeOverlap` and add `removeOverlap` to fontShell. - 2019-07-23: Added support for `fileStructure`, for UFOZ. - 2019-06-07: Allow first point of a contour to be smooth. 0.8.7 (released 2019-06-04) --------------------------- - 2019-06-04: Change `RemovedWarning` to `RemovedError` - 2019-03-26: Set the first layer in `layerOrder` as the default layer for `font.interpolate` - 2019-03-18: A missing glyph in a `get` or `del` now returns `KeyError` 0.8.6 (released 2019-03-15) --------------------------- - 2019-03-15: Fixed how `bPoint` reports curve types, tangents are now reported as curve. - 2019-01-30: Fix `OpenFont` in fontShell. - 2019-01-15: One more fix for RFont (thanks @madig!) 0.8.5 (released 2018-12-17) --------------------------- - 2018-12-17: Improve glyph insert, only clear if the glyph is already in the font. - 2018-12-17: Fix for `RFont` and `fs` - 2018-12-14: Added a `getFlatKerning` method to `Font`. Thanks @typemytype - 2018-12-14: Fixed glyph order being modified when a glyph is overwritten (thanks @justvanrossum for reporting, @typemytype for fixing) 0.8.4 (released 2018-12-07) --------------------------- - 2018-12-7: Fixed `setStartSegment` (thanks @typemytype!) 0.8.3 (released 2018-12-05) --------------------------- - 2018-12-05: `insertSegment` and `insertBPoint` fixed. (thanks @typemytype!) 0.8.2 (released 2018-11-02) --------------------------- - 2018-11-01: Change to using fonttools.ufoLib - 2018-10-16: Make compatibility checking for components and anchors more precise (WIP). Thank you @madig 0.8.1 (released 2018-09-20) --------------------------- - 2018-09-20: Restyled the documentation, thanks @vannavu and @thundernixon - 2018-09-12: Fixed Travis setup for OSX. - 2018-09-06: All tests for ``Groups``. - 2018-09-03: Fixed ``font.round()``. - 2018-08-30: All tests for ``Image``. 0.8.0 (released 2018-08-21) --------------------------- - 2018-08-21: Changed behavior of getting margins for empty (no outlines or components) glyphs, now returns `None`. `#346 `_ - 2018-08-20: Add public methods to `mathInfo` in the Info object. `#344 `_ 0.7.2 (released 2018-08-03) --------------------------- - 2018-08-03: Allow contours to start and end on an offCurve. `#337 `_ 0.7.1 (released 2018-08-02) --------------------------- - 2018-07-24: Fixed bug in default values in ``BaseDict``. This fixes a bug with default values in ``Kerning`` and ``Groups``. - 2018-06-28: Improved documentation for ``world.AllFonts`` - 2018-06-20: Fixed a bug in ``world.AllFonts`` - 2018-06-14: Fixed a bug, UFO file format version must be an ``int``. 0.7.0 (released 2018-06-11) --------------------------- - 2018-06-08: Fixed a bug in ``__bool__`` in ``Image`` that would fail if there was no image data. - 2018-06-08: Fixed a bug in setting the parents in appending a ``guideline`` to a ``Glyph`` or ``Font``. - 2018-05-30: Fixed a bug in both the base and fontshell implementations of ``groups.side1KerningGroups``. - 2018-05-30: Fixed a bug in both the base and fontshell implementations of ``groups.side2KerningGroups``. - 2018-05-30: Fixed a several bugs in ``BaseDict`` that would return values that hadn't been normalized. - 2018-05-30: Implemented ``font.__delitem__`` - 2018-05-30: Implemented ``font.__delitem__``. - 2018-05-30: Implemented ``layer.__delitem__``. - 2018-05-30: ``font.removeGlyph`` is now an alias for ``font.__delitem__``. - 2018-05-30: ``layer.removeGlyph`` is now an alias for ``layer.__delitem__``. - 2018-05-30: ``font.insertGlyph`` is now an alias for ``font.__setitem__``. - 2018-05-30: ``layer.insertGlyph`` is now an alias for ``layer.__setitem__``. - 2018-05-30: ``font.appendGuideline`` now accepts a guideline object. - 2018-05-30: ``glyph.copy`` uses the new append API. - 2018-05-30: ``glyph.appendGlyph`` uses the new append API. - 2018-05-30: ``glyph.appendComponent`` now accepts a component object. - 2018-05-30: ``glyph.appendAnchor`` now accepts and anchor object. - 2018-05-30: ``glyph.appendGuideline`` now accepts a guideline object. - 2018-05-30: ``contour.appendSegment`` now accepts a segment object. - 2018-05-30: ``contour.appendBPoint`` now accepts a bPoint object. - 2018-05-30: ``contour.appendPoint`` now accepts a point object. - 2018-05-30: ``contour.insertSegment`` now accepts a segment object. - 2018-05-30: ``contour.insertBPoint`` now accepts a bPoint object. - 2018-05-30: ``contour.insertPoint`` now accepts a point object. robotools-fontParts-26e8b8c/README.rst000066400000000000000000000105131477533125200176350ustar00rootroot00000000000000|CI Build Status| |Coverage| |PyPI| |Versions| FontParts ~~~~~~~~~ An API for interacting with the parts of fonts during the font development process. FontParts is the replacement for `RoboFab `__. The project has a `MIT open-source licence `__. The documentation is at `fontparts.readthedocs.io `__. *This is a work in progress. We are still working out the API, abstract implementation, example implementation, test suite and documentation.* Want to contribute? ------------------- Thank you! Please see the `CONTRIBUTING.rst `_ file for a guide on how to help. Also, feedback is very much welcome, please open an issue when you run into something that you wish fontParts did/didn't do. Installation ~~~~~~~~~~~~ FontParts requires `Python `__ 3.8 or later. The package is listed in the Python Package Index (PyPI), so you can install it with `pip `__: .. code:: sh pip install fontParts If you would like to contribute to its development, you can clone the repository from Github, install the package in 'editable' mode and modify the source code in place. We recommend creating a virtual environment, using `virtualenv `__ or `venv `__ module. .. code:: sh # download the source code to 'fontParts' folder git clone https://github.com/robofab-developers/fontParts.git cd fontParts # create new virtual environment called e.g. 'fontParts-venv', or anything you like python -m virtualenv fontParts-venv # source the `activate` shell script to enter the environment (Un\*x); to exit, just type `deactivate` . fontParts-venv/bin/activate # to activate the virtual environment in Windows `cmd.exe`, do fontParts-venv\Scripts\activate.bat # install in 'editable' mode pip install -e . Roadmap ~~~~~~~ We are currently working towards the 1.0 release. * **0.8** Initial releases. Python 2 & 3. * **0.9** Python 3 only. * **1.0** Documentation and testing complete. * **1.5** Removal of ``Deprecated``. Released 1 year after 1.0. Testing ~~~~~~~ Testing is setup so that each environment that includes fontParts can provides the objects needed to run a common set of tests. This makes testing very easy for environments that use fontParts (for example, see the fontshell `test.py `__ script), but it means testing is different than other python packages. Automated testing of the package is done in the fontshell environment. fontshell is fontParts for the commandline, implemented with `defcon `__ and is included as part of the fontParts package. Before you can run the test suite you’ll need to install the test dependencies: .. code:: sh pip install -r requirements-dev.txt To run the test suite you can do: .. code:: sh python Lib/fontParts/fontshell/test.py To test in other environments, run the test script provided by that environment. You can also use `tox `__ to automatically run tests on different Python versions in isolated virtual environments. .. code:: sh pip install tox tox Note that when you run ``tox`` without arguments, the tests are executed for all the environments listed in tox.ini's ``envlist``. In our case, this is Python 3.6, so for this to work the ``python3.6`` executables must be available in your ``PATH``. You can specify an alternative environment list via the ``-e`` option, or the ``TOXENV`` environment variable: .. code:: sh tox -e py39-nocov TOXENV="py36-cov,htmlcov" tox .. |CI Build Status| image:: https://github.com/robotools/fontParts/workflows/Tests/badge.svg :target: https://github.com/robotools/fontParts/actions?query=workflow%3ATests .. |PyPI| image:: https://img.shields.io/pypi/v/fontParts.svg :target: https://pypi.org/project/fontParts .. |Versions| image:: https://img.shields.io/badge/python-3.8%2C%203.9%2C%203.10%2C%203.11-blue.svg :alt: Python Versions .. |Coverage| image:: https://codecov.io/gh/robotools/fontParts/branch/master/graph/badge.svg :target: https://codecov.io/gh/robotools/fontParts robotools-fontParts-26e8b8c/documentation/000077500000000000000000000000001477533125200210175ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/Makefile000066400000000000000000000200011477533125200224500ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FontParts.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FontParts.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/FontParts" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FontParts" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." SPHINXAUTOBUILD = sphinx-autobuild ALLSPHINXLIVEOPTS = $(ALLSPHINXOPTS) -q \ -p 0 \ -H 0.0.0.0 \ -B \ --delay 1 \ --ignore "*.swp" \ --ignore "*.pdf" \ --ignore "*.log" \ --ignore "*.out" \ --ignore "*.toc" \ --ignore "*.aux" \ --ignore "*.idx" \ --ignore "*.ind" \ --ignore "*.ilg" \ --ignore "*.tex" \ --watch source/_themes/fontPartsTheme/ .PHONY: livehtml livehtml: $(SPHINXAUTOBUILD) -b html $(ALLSPHINXLIVEOPTS) $(BUILDDIR) @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)." robotools-fontParts-26e8b8c/documentation/source/000077500000000000000000000000001477533125200223175ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_static/000077500000000000000000000000001477533125200237455ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_static/fontparts-map.png000066400000000000000000004175771477533125200272730ustar00rootroot00000000000000PNG  IHDRJS6t sRGB@IDATxgux:֖ػZ%ֻ-ɲlQ2)bAd`0nηߞsk:{jsT:/ă#8#8#8@tspGpGpG GpGpGp"Q#8#8#8(pGpGpG B:#8#8#GpGpGp"Q#8#8#8(pGpGpG B:#8#8#GpGpGp"Q#8#8#8(pGpGpG B:#8#8#GpGpGp"Q#8#8#8(pGpGpG B =8bؐ^R:#8#nnq}"STrTӖSqGpGSHƧ1z\OGv&ɓd+r5GhyGpGh5~Ee"IJ8dl2dCjNyl{|;#8# NaNAe6^GYƺj$'ʪӦ%ki#8#(\G B ɩ2([H1cȓ|5>Nc[NGpGhGֻva'O W6O#^+Tsx(EVe'pGpV#WZmoP@ '_JKPBqA2k)'E]A-Պ# K1uGpG}+Jc d"@'O)]I9;}Mf/E„.,BROO29Mˎ9(;B~:5㖂OC6WG+^9jZBqޑ[M-)MU&t:HoIQGpGᇀ,=</,99YJ;ȻwLn|粯0%9Jvev85/~7ݵ_~^z#؅*~235u6_ߒЫK{ÂHd=;hkl l8yl}1tn2qA[޹룕$Q"(kN#̓#C l<)'G|!>/'E=I,yX#Rl[#xQj<@ qI:?}J>ҿ@' ?wHWre ;KxB$EmEHz&.Hz⢤fFJA:KžmпC6햅-B]8',Zx@#pGz.P,Tpp]O>aYnG߶r%:KtpGI r|@' /Й271ni˒9/7ɵ>,ze~:M9cCXY5AҘGj'j7>>7(mK{?R ٓ ]zCX~lʍ,ś*$$[ ڑ:d__z];b*ӭ2B(>qN@?b2q=5Fj%lw.^'T3ἷ!. q*A9BR3/k}I?ANaW>%ũivGmQ:bF]cQ_JAyX=NW*Wbշ% 1`i<OzUayOeajq 6Xv YoLٷԳjl@_PgI?Ncu;JTG .fH aytr.\Rei $Z܎@sekvZNR_ $Fax"g X#RG|oDc>IkjavOmQ: \EoդE>rvz9</491qnS'į?i)NUbS#p#椭@=SN2jZ,Nv7;(u\#Pmq \~a^V[ pW,ě'OJzֹ`2p̒r,_ܳ_ܰWivzHp#fRGiNV_/ncC>g?(\6E ]{\& mqgԓ2\q#a޹ ;gv^O\|=xȍc__;Xbj<۵금׷u}%w=w(u\"PmQJ6PtA#WDvcܩ#JJq 0?+]O&ynvN?~f$WV'bV$mۭKβJwI!`8XfSԋQǮpJ I3h츍yns}e]'f?]I]doɋz貒:e9}&)=>?&jL_O֞Ԟvqʼn'P̷杫kd*?,W&K|"Pk|9|${ Eפ}^e,X& *^emH)VW϶wQ!RUmv1\q( 5;9>ܢ nm`m(]^G`-JcyA?׀{Xٙ=ʼ`(Sblk Ԧ5>RW <j'Q_^+Cc}طRWw4 =P{};fnΎJD'$fcY ZmPJZwI#Ϫoh,lA+śL v픬t]Wo P\%p&v!EGUxhŗ$3eԱӏVlFs:XU.^̐? oo;Mȶ#Q 뿻g\抓pjyuXIT C+Wܽ'-y}"ʣF~p2m]٢n:G,_PNowo<-͝{s:!K/]UG8/@F~-]E= ^vzQa15W?AE45+eKYk-$#β˦WvЙz/̷:p[6Bik>V95jbe ~7GÏ8.,!LjK& mЄ G`U$ױ]U}^6C/[+s爘kSQojPzNk׿^c3mz[ @., ()=X3 ZqF=A)N֫KRzk] _Y>䁓_-7ہ5+oYICJ:F4YDzk1 m,o84=И,sF+?G\,Ճg5$Oy_oRgZg$\ֹԹ se7_y.&6^G;6Ʃd<3* mOwl6t#l(ټ=N|K΍3{JAYsKpruNB&q%xf% /}4Zm̞~N@ۿM R5섌6N<ԃrƫAYyKm}C 6IK qUIJGO_􂿄:|_ /h=bd/(''E\MSiqp~2Uf$K9uZ@8p#?+Pgô٥vx1eޮ?"9UCIo\f!RS;tmqI4Izbɗ>dSʒ(ڀ F?1ekiTE+v|&1 NT_:o5ۮ };^Ϡ(325+t@Sф G`9qiy-?EI<*_bSoO ZB_[t cIGE1qܣwG$ sMkF_ppsayћ &"PVNR(ӨR  Pw7dh\YVЙ/Z(DGcq=uJ+rȣiH#RڳM.M tV#7,&\4#VwG$J&)xi =jiFE 3H9_=%;Zg)8s`0)f8Usr,e|ZJ'%\KN1NZ_GZkr/WҍT_7'^{++JDFۜ|,8;,_x|kp+]<5b8gM0ս#L`&`o DJl ~`V/Swt R] >B ?* Ԃ _NZGRpbͣ[2ۛט׬3~Ne&m$YIN鹭GP9)-̡d?'0yH6){AR3%9e̎%H4ۺV2c+nꑅz70J?aõv:3:Uw/>>hb'X&BVɧ}]y /JfVoI#8D>]raa*mz#? qfĩ>yInFUKG(NIw u'hPuJi?2h7qoXŠiIEWeP/{nI.=zJ aȄz'\.JAk(٫n'OY:G@ujfn"Z[IK'I$gQjJ ad4ب1 nC>[-m;eس0;[.7aab,m6̷RQ6?nw̎s|LqO|I34Vvc㖾#{'"Q+ ω;_WW,Z"TJLxi@9 &se,},kR9ns7ߞ5o3qʝ:BsvjvU*,jwH^;ު[NG==ld>OPZX: ݁|فΖ֢/סh~2lbŏ0/ !5ҥ~{uFdQ`Yuƒ|$5̗o?/S~R#]ߵ:>D?ـ牀;JD (O$ Y1 }73zi^&L:&ߑ{%?Iߘ6噔ժFb։⶝=h'8KXZP ~"̀ae1i*Iڒ.}+߬HǴeFEs|J6@]zB(JPJQʕÒ~up|$?Zz3*Ӓl.sgKacŵ(\jUAGI]9m~VmnIYһIGc2pTh:? [ߠYJ Rx 'Ei$s>y *$wES2Ò+d1ֵ!=IGAYh/֏ν&)=-HΧn+Xa%=72N;wĆT(dWNmKV !&o<,?#!}p-JU'̈́+LHC>KeiR_zL-_m=(.&,6d^eQHӰСF ]jfۤ9+#,8o=ؼ2ༀ./w)KD Pذ:"7E) ^mn۲+2O+t0\6Kˋ*}-9n+y($X'~lu_-,Lf3u{e~W _lO)3+nz2?'MGIFoC(.$7oRe 7$y̵7ѱۮ/ o?w"Sڵ_?wZ2;Vjl@=|cCx{8JzP&A7gi\D s+2r_Qݞ8Cэ Rnݬȋ I ކU m%lrqʗZUITKm*nCpF&Ϝ̌ j/p /ʡN25ᧈX&jVb,ǚvW6_U?#z68 e'ߋ#4/@(LO'v^+He޾ xl[a[ MW^ӂcӧ-hpfGBWʃoOW@f%>_:^6Բq4[ţhe=Ii)£?8rih`x)ſ;ŋIh$,3x^Nl/; WzP>:*_*^MG`ep^bb [Iq禂 ;Ui ZlkE.ͻpxJN;<O464姨oC6 F)vX|zP`[ҧ@.~P5lVކ`O)p|V;7Jábܕ.vw~6^eBy^aզ\J K>j<\ g(Y&OPē^Aۯ=nhdB/daZ~z~eY),6}-||9>S7-x{X5RcYPMeZ<SĢùs,N°nU6v@TmJuërk5Ox. _8oʕR Evmx懢DAZ(7!ŭ;d4}$K=rx >ڽOo;+7.KQIpܙs*{/ؓȖ;G~p)_mz4ʵdAl&f?I7(5>I'(e<$}w;'kXj[4١oILg mVT2{|mOU3t]'mw?G1f#*9"%ꄿϽ^1HK5wʦ[^tVn f cqTO}S}s=2}U~kZ3]b,Z$o:OWw7"=-~|OȦ|^FP54x[rLB4H|;#?wЗ0 K"D9283LJ}H_CԵuly㽲w˂Գ +3:P_FyZB5Gi-uzٍ~ ,'O|:ƹTwJFﷆsQ)=eKQR؇GA kT~8e"_ /ʀqd@'X;f8&٤gsV9s 9|.oWL]38~;)<ϩ)%lV</ݚ۷­XgL47)ۤk|uNǮY=of0wwuMre޲u SVgzdPא{jЋur g-g/˂!VLZ}G MvK:x&;8@%`O%9Ɗ :FVxEG/N,aљ.=7NR>>Mٚ$?>Ű7 3'ϼ˥(gzHt:JQ bV˷5Sr!Vk@=8\ȅ\dK]$TÆ4\(Btp#-o* I:͆+: Bȣ[]l3zgƂS)䲥/ 7;,z'3=Y7J85.9]{Ǝ6lmX6kHY, B%W-&3@RiF  @e 2]q uyك#VQZ+,υRIٙ }[<\F ʭUp!&:KĘ-+WpK`NNyn+hų&!pgϽ6&o)2N>>9BsQfwtب/.xn|E@]vAP_-9J|T2߿e-Ž#pG !^,ÂE%:ٟi>a1|J~ߕ]z[ t m)"-UGQh=e6uݯq=KB$Y@(5vY%S:'JN2# ,wRpJjQ/.`@G*D?6_^)W3M[|+"cIsOa|}W􇤌~kAE[Ř]7)':Ym x)n+NrZoi wh0!<yuLꬦ&w7'W}B]0ٞvE9ņX8 {+G(OO=h:]/?sTj'E_A; wJ/-G/@bõxzbyp@+BK ݛK;i܇f6۟{$, `c鈣\1[ e&Y,FD̙a.īt޺z r|eݷ3v2-ȗ؟q@#S~A쮖^9-lnT/韬`rF䲍 ŶImkVfyg9<.eփ{ E* G"Hd?zu1)hKI+dqy[~˵\f^αx\&ۼo\Qj1& ύ'O&jM}ikBG$GG \8xPx?}JJ1e+-W3}*[]xڳ~]AYRASXʓZ\~e&kR`R]={#P{{رqy@,ӟBk߂I@o]p:0izia,ݩoە+}^mzn[v #08V;uZ@elFzns w5U}5e@W+[-o[Tvwc~X~زWRjʬZywA& vxcMd}4uz|u{G a&ck+cGoin/}~6`z󮊓D-K9Mlvcܩ#L8v9I-Άj+p&cZ)ۋ)ۙ_+sj}aŞ-Ŧ;w4fOⷊzɁ-~S%ֻj%v?[jE>쿖x#gim;LqcMb}55]8Kdos@Yy5 ҹZ2Pn-}{庞۫UrE[,nI<1'ق2h=H+|WۯBgbՕy1,>ř'zI+uwܮKɫA`bok]L#Rld7vlP??h/XDWPqaL&=?'۳}jnn%7ܕdGb8yKۭOE!1;ㆍ Rz>=9v-lڢ갭$jۜ_OQ|[k_>y}E& O~m6vgqNbrM7SV~'FcTg;pmj/^έoTUxR ahمeGNؓߒ. J Mw˻V\"VIԂ4@Hw8qq?fn3_/ÉŜ|^Z񕂄h!k1nW]WkM jurw`Sn-=6yBMhB <'~gNRDȓb>i*'$ILH@IDATdzq#(m<vCK]G:jlղb>&֖&]޼mLq j4w77娚|i# hycW+HWHc0n\H ^LFi6|tTRM})K&Y |܃#J869mu%WE.q8Y#uB3ٛ9$% rxы@ 4\")J=wU5%߽5b[M")[ e=ut;낇#0ov--o813vg&t[ޘ?2l9%S )E92/SJ>^Vup]_IxXYL4\ +׎2󖸨Ǘ~ۯpŜݯ[NsGo<7uS {Le+Im@ǩ<^U*d%/е) io2 )l҇FbKLEs#`pGɢ&r''_XCK^'X~sR|jaE1圬uw[Ad%^/?R_zN^/iyƉ3z>8ExJze' ՜~7ihǭsFU6-I-3>n"ߣRzgmOjJJsDٝ񘞙 GԪW ~ #PgR*)ۉ<7I΂=n Wg]ٓ <,Wbр;F1e3RM%]wk։!h\ 9YB:A.J?w?<[i(&xMP:U%!DG ya6TJev&<]$Î=[UaRgB|Kj}/#qw-~Mh'8&%=c|ڱ~-x]ԥehؿh^%< V1}j9Dw,nkKrSNN"2̝ &a!eȸՒ1-^%?"K Xq4`ljNvCzc>?pײŌcZ}F:8@f#~bLuF),$S,D`q`I h`c Yy=éos?lK ҭ]o[(]}$(I,yK-{p I7H91!A~vZ<'/ZHeԞJ*uK>z{"nڝ_?t^6T=Z7u2j^y#`uw$ R'H[5d8NΝIq"_2g%m.7o^ r!?,*O1]n;IX,1ǁi#O]w3e8"-9:]r鮰уv"O1 c Gg["_1ιiCcl$7=,ڬWUGgm9R"i3pē e .ɍ I@|"RmIR8Fdfa-.ddkfet~TCX΢ڟEa,*ۀ-,RlrgҤ։ g@9/ul7Ƚ9^r"Wr}ٙ_ٔ@wnL@c(1`yAHQoH15]/~^fIWsv$Scj';qh'mBv'xww.g{ISVn!ܩ#@Q"kabH/͏G\'_]|L3WLa?pjM#{[]]$ȃg/y)?Zz@ BpVK&6ENx6QAgʡe~aN^ATe]h[3Hr {\Uq'ҘG<`93X2@ s9ǬH)d\=/Hnn]2="7TZW17GFu]oR?r_fo&I^-({w`K.ٞn>CʎT1Rj :2t%qs4)B;^$;Bsq 2+^YD2RAmAqԴܛSOim^ūnKoX9^ QZȡTb}vʃD槥XP]q(86K1(qE ߔ1׭wARBu'I)da:ō/!΅ -uҸFaB:\<1nEy~PMr({\S1G9"s]¼HoOGY} ydS?t,yfzAgKݍ.lY.-l⣚df`6b}['7J1Ƽ#1BjJW5; sH$ ݹȖMoCr~撼uѡgKOVx߾&t\aNް6+'+RqAumx)'OG&gV>x҇A0ٰ@j``4wEi63&z!P@X:8! BJBDC:TiZ>NjuZbq@'!qn+vt>wץg-nybgn\ 1(yAL)s i6Tnj dzp;w(VX/)n*C}]0lm:IC)/ }wɿy勧ͯGK ut??+{kcz[Y*-e;WT,5Anb%)Am4p ,(ul1vellLv!]]?nvCLtm/Em<N@2{AkBWކ~~;u+wI\IF8qf74_7Oof7Vxj}qۼt#R!\!:Pyu Z^xMO[ny N9ۻ߾7͇+r֛ ~;J+c-Z_x'ebnR>}ҋTz~:8I~+/~N?'>p{vlJBYߨQ3O=mtN㖂G^,Af8|>//b(799)7xch ҘGtX-r²SGS8ǹs|떋vѯ0;n 71E=#R)卦ڃz3ԴNKjXYSߪ+i[I#0A@JSG wPY ݘڪZ ł>N%n%} +H UƯW0imׅ2ؗddzL⥯GdA_M*WP.h(X]>C "R禯1 4AcrGGȖe:(1嶿1:?_z8ʁfOV7_ƋTVٶG_Gy18c%ˑR>m;O/]hC9}b(63`cϴFRi8(H)8TեuF}j#j,m Aa$VHo >Cr߭Ac}ٟkoTzR9;vyppGiX*Ľff:*RRЄ42f=9':Cy9\<}yiSB}E vSa]T7h3.kvM\(|kIA{+I*dך #mNlveXVje3JX_oYt\Q&!C8zhXOIzy#8Ӓ(d<'Q+XOy~!n!)/ Ǒ-_F^m!6framחYg<#C #O~LoN ]r-jSjcj6RN;68ZuTci<./UKԄA5 ];QK?pp>M?_ԗ5, ./:Nc![̉Y,܉oʮSF yS">p,^p}#[εc w,yN@jq" 6ٶNY2IXz6w7p#<']]92ar$6{~ OAo#{3W:9ƣP(o4;"~_-vpJ-Nؖ.'H7p`=s ҝ]rf|0-nKa_re 5yr{ ,xU.WȆ~U`ʙx_pAe޽V<>8yKWm|,G܃#2skxKĔ ,O):O֙NExFo>){nT0py8O3n)'%<5RuAtL kAZubM)E4ڐ["miԡdelFF^m cJGqԎew:YmĢ6SS,L#ry|,K48ӝpG#'N9>w '&'JO/qQ琐VL) )99Y( q$UB-Yrʎ.mpQ3d2%vOS8^؃r9YUR_S_R13jjjAA&؁OL |26/Sd)q] _+>񘢏 Iz溶(TiC\?~jo]wu!/t|A!v~{%;0l%ǝ/Bcbb"|TUQSLB?ח@jy>6P?tѣW y}7Y}Ü~vi:r|8U;6Jx@((]HAI̊j#Dbn{z+ 7IQz`Ќ$2OY(Tyƫd0bwhj *jڞ݉jN*Ey\ձK5!S9cJT ґ Vrԕ:҆m ׵:WF=;a!cYO<(CCCrWԃ6ny۸4pJMsa|8ϬSo)E=%s}7" :PVƵi1<)08vV"1Nj:"~ę"N.]Z)`W_! )+K}@^-](B)[xc86.WOJI9'b.+];ea.)\_mzX24@1c_OȺ!D(5\hH\5 a3reDA<-[$prɶkyji6_3yR2tq[>q2=/G8r8ys /| 2恳 O^ E:(RzZ >>l^+H-xz+0Sʃ#PN>rL?qf'ouGߙyb_bAmN/&ɒ'bbX'C>BfKy%{E]|^ΈRTG8~)\w_-9cc>#~1-)nR-m@#{Wxha-׮.kJ5ZeH{NYs+ғtG0c|4ƫ,S1aA>9%GJjvn\ؽC~v)9Tyv,0)-)ęioWde;ȓw;J P3OjbnceKř]i?䤞6;;+'NyW[Vҷ:WR:@up~R6aV!?6ɬiqrPaYN>V7EqX&U٘!0h;)N}=//kxCYyjqKQi%ɒN|5}c}ŃJ-ڶg]/|C 7jZ-;o6X w0%td^}hMo t%` t2*C|57?@X/덢ψK8K dyw}0`p@$CIQ][ZXG -%Z,Y-oR $ H`pfz꺏WUxˇ5~6֎q&rI.)Lϼ0-+iS1Ͻ$Sg$oņ=|}'m6-n˵uM:M#r,_m1nLlڹD3 8p@p--Z $[a(n9ၺ-M< .䑶I۷/ڵ(!n[?8>00lX2m!#m6~X<6ߝoϹzeFO"nπɅ;FġC͒(5pNBX=G[ӏDaZl-qɷ}kAZn e~\xBxׂG﮺*腼0[k3rg3Eb˰RrX&Lٶp,$"M2-/VJ긺.U-oۭMa˰RY ép,GGye/; I?ۥwqvO]̬$! K[#X=@gM[,h,J E:Npx$&vuW 'c}#Y-< m|C9l;2R~ٳ/%~t`Zu-y9Wo\=ɔa4xLnۖ&BԵ<~7::*P*#jZ%Rr,Ⱦfr@" _}!_[2QDy<+֫ǶXJ!c!p@@^ץGEZiS-[>~aX]߷((b%([cbN!w_;?:ib3`; =eVpxv[/Hr`ͬ{Qw`tãM|V09g OG˫%C]l4`6Qm[Q-SVw ,TzIE$(aIꇥwK(1=-dV>Pj: A^q[9:w~ڴ]䡞 4|ڟeq'孕|W6馛_!NK%ry٦׫m<X88HCxrwi|qy.ĉAqa|Vr]lk"bpy^) 1o)fgh֡>a!oV#rod|Unkqڜca!qX⍲ئZ_ B[ )$[H(#-R^ >)LZv%]"XpģIg?88DUSN sy#3 G9WQ#+疭yֆm>?$qŮNH_op-MW: ,D`J=:|yk:bo\.`/ 0a3l㫙[x\r~ 8ZGf%lrn h.#]"i5Z{ x 4\!3 '/l}\22DoH$ufkV*|)ZAN'o)HXt+B׏;ZJK_|A:O+G$uݕ,zZ-Mu2hv @?>|i[!;ƶT$B 8II#=0A1i9taᑺwn vE;-zӐ` ]vG< ŹLeltq.OKt%{G[q>ҜQnji䠜}XM=UzlNtI1[ 4,Cy--8 pMDa<'$:@e{HuJqǝvtn\=8R֏7h\3ѧx}?w`5&UӻJ;/ o)5a}ΦvsRs23(r!_LSo?]V/U "^{-,qR& pH5qܘMBa4ckC_w^a|>w͞wT%0 B4-tq>y x \)4Bp+yJ^n܎[M}[n |} P+RFߟ}^]ҦHtD\:ֵɼT{^( ZNhNjDɍf3|}Qy"}l߸&^.lab/N;7pٳ pBrR7k%[1:>ZMx2,!Q ׀ҧlĘW/![4EOkCH"n̞ē ]X2[XB9`ErmPKAehBFZS{tA+w( >c_%n|n]̉ =Aw'5'7'xߗbb:%&W288@sa!h£x˞&'!8fap:uV^>#n ]3q9tZ^#u&.N:l$OCёjtnlCYJv۝znvs'N(E YfC:Fj맞DGW ]SG_(PkRlv6hFh`*;}E'Emi,ʵ{>0_/cwwݥ&y렲-ЖڈxCcҫOeٝ;uDU嬗v]˕sPj=Js'5 db'7x||`]%HrmdDrN4;lSNPPeovC_1d8mqMBi 5=}kRЯ1[Ls!4Z-кHiBJ>bn38ul<-o1% |l>@wO>#]ϴ׫ ?#t WK-{s r9^; Lj&;<;ۀyw*{^NLȟfSMDvCwͱ Iv㝥ǏK>&W_-\8@#v|OIAO`K|Bz:wV.T=XXa Nz%qJb#K$7M Gdv["`Ź>r=$ sqZu!z+Om[w.(ѿaWn((mqrY;]쵠 XgGr,$nNݨrmPN0a][@IDAT|2 n ȅ>yX}srrrԠ87H\7*>>A< BemR|MgP\#-zY26D>m8^z{8pCq$ ['Ƀ\']= ?^Ui7C-fKdCTJ^l0Kl,(m fnSP=2GŽߐoy߸{<^wk=~läi90LxpSSS+r9hiwHs @;)ϝiSsY`oQ.YBZ^sk-s}]+ntZ{="^Ԫ%}ֲkyUѓo-BQ7{"Q`!|ί5a2{mV@iFƞ`' Xȅ L)y3wFNLiN2z\=$ˮW!n!Z'IsYoyQٹsgUa&>[~鍑ojՅasX _.vGŗ\_g7--56iqED54uL{h>g`X:xGU@ARk7γ>PDGNz' -υy-|1Y3!*paB&32Ud)-v.闝7S{GbdOǎ_ 7L┴;yH_~Y}xo&ڀ|®sQd*uU=lr ܸƥ,;G"t "z: keKJ%0ij;0>nzi}H$~Y30-ݷIh{J|G-?jǚYv In3Hp2.q !b+4a^Z-fr $ET6Av8I, =Ë/(\.K* % ) π\lM(Ϝ{-P[7 ~LӃreDz;cNҧ)2H II۞c@o@^(C:q@{R"hq[$oia#&ݠG]I_r~HF79)cʣw1}ox_'ئ!M :&>p0dvd~d}.,wqL4q@'m$em} >@~`񑑑} sgN i-qovy‵`Įv&ԙv1ÖG8'}_I~`ܹ6r$3Yn= ԕM~Dwiu 3:P df>PjQP8t Ŋx]Y(2úy k-^@_βBZ}a·DP ϰO23!?#}z(c_dnD[x;$C@ @YÎʹD/뷑&[+z!H:?-W1I[/Qe;+ BʤOcveS6W6D+D:6ne+˖Np݌|Cubwr{F2ұ0%%8݄FFh(5ʒ cK9+:$!֢Gb ,Y:z4`>qMn_z9vXw1ʎξIyҷfl;|<]7J?<|q +<-ТC}a ԝ:JOmk[1侏ĺ+乓k"N>‡,]`~Kd2dׅ#"[RƖ*_}_OW eYmXżgJ>: 3){ǭRһCo}?B}JQfӫ6XiQ{(ءukh# wEfN>PjсɈ[I=p|HW uD"v~Ǥ4V~t6+#Dϟ:#3nElwts~3C]*A@=.tH[ umr.dwGBHk7h,ap)9y}֧-e؞ XqNߴX@[еZ5c%|/Mk,t*$KwkšQ#K) 8<9ߒ@¤KIIhDNܟ}.]kRQ`_4kHqKEzeIOotQ#ݷ7d$͚mmwAR5oKc*Oy@ <:)dwK}Kat\"w:L?R*-;?dnJQ'sb\_Y,?W}5zߝzo)-]#ջtÆlZj-2.\p:.urpˇѮ|[8bq| d?,xHrA )t|XmKǂ;Kpʓ,88y&@X~d!}sc I)٧-gX~\G"~8D_@7*Ւ6;k*> HJ?Sa_I>IeL^k^}'%pOY-F@rzIXvOf7*_.T5W RԻ(];fFnJK /BTgCJ*I^Do 33IHQd4(`sθ2 @H'~R. EC6/jbGtr(`Ix;qw yLEm{#dP&rc`\HiIJJ!uۭE)H^4c*V~^xQr-m˷Qjb@#-hq!M۾[hqX.]=e,cdUy̧Tֳ@Z)}=ӵ3|~G?MJ'[ ۃ2>$5n*7uI J$p=m7l)jS;2B]R{9 pg@۱] z'y)AE1kS DB|̴nD#utu })39G6_Q R|LDB97̇4|;RNur*#քD16v|Қ̠E rJcoXm@ņL%v|Nz(ř_ZpjAv-z Ǹք$K XyߚH,w_ϥs8ӯQ~ 񪊞h2 XjkA8)CkqlubMo,ߕ:?,1$KBBVTwJE NPtnUp M**S7p@JIՇ䔜 0?N⊪[Ӽ(!.BY4x-qCMp, @c}-ӌ^ԌZ{*gtH^&N3YrpʨCy'NX+rՂН' x"3A'uK[99vr*XDk>۶=-V p}I?*RuzT%e= 3~gC %}¢ޱY*] /Jܳ/KDb3gFc2{d޻C3>f^xI $VMn1;G駟]|PvcsStthcV+RHI=/݇x tuˀ]S>0ʆ@P5GAǘ'$OdYͫ<"w#`>iuhu~1I<[tX;"v'] ŐeL.|')[_馭<\ldk͋FB[$-dB-^΋2dB^sӕ>!]SO_r?Wϊ A~D`׺mӠIAG*]E)V F߰nD?ʊH%7% &'e36Ҳ=JJhc_o,=#Lh _~^ooK/_@}.إn2?]ޛȎO*lpŶ-,?gye$vk92Q@Ƕ<\V=W6id!p\WYCof=܈~[`#-`IuЮ|FZRFFjM $?(mk#JN[+i@%9tp\zH_ݡ͉hp AK|gX*;>z' SoHQ zwf@tF`&:l%@'e)ѝ;tgT4BrP&myЩEwF*0t|J}nApfu>~H/Ͷ!oDu Lnlhp0i,ࠑP˜ ʇP?L.y,8^:W>H<\NL|S:/Kb#1~I:6K E&з}cjM?*R9HuH$F@,h@R/!`.ݕ.K=rm  S]||= y3 r6چI:E033l Ӵ|1AﰄK>lP `A ,B旷96Z9 (͵h3Jm6;\0$vG^u ɨ\,,@O'~g&~"?bm)yaO3$6hnXg 'IKlu‹o7 La %ev [r5Ӡ@G  ҀdRHpLtȨDsy#S(A 94b/b^[Ȥ ^~<6!vf=6+MaRUJe&huL.ZPkI} rnP^<ǽmB)-/<:7ߣ@e4 >f& f[`#-umuxGcVdH1%ߓٽZH@Y5Y7^QAT;3kePApG œ@g尡 Sdx0]wyEQ?Iʇ$T 3mӦC*˰&m9ȇ~8TJԏum ` 5!&R3o ۷0N Xl~+_F^}y .+#[ÊM 0U (2qo]'ѮV35M%Fjj s{I|O%w-毄^$K]hM1`c+qذmW+ARo7A3V%m Rur>l}\-#*Bw VןyofN=\fuf N~g%Y}yoEJ\vO 7s=3h/Q#L[mE׶J)oUXwIʪuka['X*T@("XҧT"R&"^U]  H%租blFv\'}]{*'_I4ˑfeZLmROq[{շnݭB. qBg%*& OP;}F 7$}|m!緕 <{~{5fׄ(`AgNU9+ӶiUd_l]IA-+ypd}u+vp^)Hl?(㶳R:fдmaٞ+w$ ]{Nf'tn>nTB8} Te_ ֪OC;D-&1^R`-7iJ-xHܕ@$QK^XRN EHVF aXVYׅ+ܲ{+=%dqZ{ZGkԱĵZ8Zynv 8,V0[`*A>H?B=`79jW<]^.;tmA~ł[wn߿M@WV*sz(DhB^ww2,20-M~2}E$g9- ^XL\yH^`Mazm" lI`sey4M( Q ]9iZ2Ora4,zs|x&7+ }]Ѥ(r*w40_ዜ]lZ-H׃6.(ז l0DV!A}̔%RFzu7)(R16+f]9#=I4]b#]]H$v zgm9:p@Ǻ9lt.Hc_zSOB=NTӧXCo m|['~@$>V#^rnB2,H*9([dRg&]ec]ҥ/w޾ pQ M!met䧂FZ/dz׍z+k%p1) Tڀ =wrZLKf,qVJ,%A#: 'p6MOsR(/(kPOgr63?,?qUNkeˁIz_K:&*L|;.;w|l! `cߔ2p7w&dB%⻥fvt  }Cgy&=+q$R>%1n6ءG+sA}Տ MOOw `:|:uJ:}Io~P\u^xE3*,0~j-6hXwX[ ZCs̸Moc[-FE[y}Jz~}^yߎ+)U-2t:B%hisXfv<$o <6T׷- w]_5q !'L׬ExRtXMwwq 6mO |Si菤sVٷ-PWttZiLI@$Bx'Z=*۷o{\/uO?) d{{ԇ?x8Ǖp u6JQ#sWS#wx6dh僻\.$IJO^ؠm r=P˷-PeSߕ[ n+!pkFCoapn 'l7 eGdr25aSz>jZ%7˷L1I|X3uق7$w>'8cqY3K___)?Hر#KA9|H2]Ǒ1یǗiѣG$A ,^K4ʴBj;J L-v`>19(_~IusfN|d=hrwǃ#!Zsqj,Ю?ֻIPu6JwWaw0?٬˒J{zz{ 𹹹;H]]]k;Qw;4HGtkM3J 5h8a^ww6zD|{,pj{rK?\TWp&nyh6^L-vdc26MIg&ILLfm;Lhnb13QxZu6kE]?fs cE>!-(إFvqlozПeuI`emuCP_޻wo؃Ҟ={ {5͝wH{vJ[5VzZyszn+G' Ʊ#lK? p|&N-D ա_C@l\lg[N>|S2{X, 0Q//y-4{Z2D|sf* S/r혙:&gO~REd6釩d÷-,Mmg8y m{Ʌī׳^vk[;y\3k5d}z^\6q y1OXػ[&ic\WVwyWWڎM܅F\K-{GN* 0 yKjOvͱ3zx<;{^nq2ё- e^pr/]jk,[$EdG~_EDKyEo}y5W2$ [Ƈ >< 7TQM귘b]u3j1"m!gq[Qr5= jh"^<7%˂D(,|w9RX4yŹe*6B-LJߵgI~;$sa Ar!v%D|,sD{;cy]7whڇ<\}@Ox 7-| }\\`ї-D,N^#O] 8!ׅ~hٿwz~H5RVQ !3STmQkiSͅ3?*mƐ6i+J2 ֠nku m@@8{ ҺOpM޷jڐ6MjI\d'<ybqr:=՚# Ŭ BOV.0 m䟙:hEz 4„Lߨ黵 [Fz&+ŭn\7\8zTdej>TG&k3Ӆ9u@;axH2[;So+퓽X8/gc+YݬFZfNcq Lly9?FXfhnsq6D>FC]rӟN573K6#:gQu]̧%ovVo:?,Q*瘅ȣ0.΋{B<2&=2wK8n4v2~ӵhok? Rj'<<gOHU#ri96{qщvhC)f^!b" Gf_[H6R/Ioc7:`۱xؑO\ugaݱ?ڏ,h;Zԁm&<;dEu>B4NF+V6۳@A/V.dҧFKÝ٩ChGv0fZ!d38%'^aQRZ:3g^ !8Gr/h^@ }=>@[q;I6@x\c''L /Scx=[zӶM/.6tk o-)>yk1 M@Gȼ0h/kg@@~TwÛN)caM4/{|߻*[쿋N֞ԁ'큓3I[SsdOUyHnZi6|!ݽVѾ[:>ѧ\hdxA=y]\-Q/WUrmIqi!mT`>)-G#el3Bʊo]myQn!p{bCZﶜo C*F:dHw;󞻂 ݠld$k_ھl>LN{s?rw Bdy^%(ߎ+K1oB-2d{-2ʼ縱VqNYyD؈q$NFƆ!d#m{I]ծ[d݈1QG^@6sdF犻VBY7!;c瞥-T~cii斪ڔ=;m7JN``8!X:qڍ)Le jt18?!sbb}k5L>mLW#ڎ\JJіMnۣ~&k^tK W0G2$f]M3[7=G{ւk5hj-io@wCs>Mh;#^]㢟|08\+or#ʔnMZ%e{G.ݒ?BbmFDH~¦ A9Y\LB:CZbjX=_]+`req"t q]ʞ>y-0~}щr.Mj d: WW#vK)tG~\ hm q%ym[KY8_[`-Й;_5\' EÔE݃SXP< n~@tS Gȉ/+w/myvY;nؔ9`0l.㱡īrk2?NL 0\LnFf >y &;RНW(Yֶˋ'$r $g*=wHG`!ՎJ1L# kݙ/RHUZ7~2sR}&z3Y "8el]߅,xSJAǛQyj[˴Ik %8g9gRrKMm>w+dVy)[-6djIASȎ'X&{#q}+GyB0Aִ@W~X 3 WW{zy I w:۷ɸ7vz8{A:+=SUAK˔9JHs >e€hM(qRp+`6 ߛ>!n;,=)'@7d6'oE-(ɬ~#3vCo筿eYyV>b$my|Ȥ뷕 '6vL_ISW[H斸\uD);7$C#;:w|NSGfKwãst^o7uG^-;^ȳ0 _J Bx"9./&jeZ}tfP>уrg 8[*NX"@[`&3(z5ĺRndfI[㫴`0n8`s{ [Ig뀇J*oU t]Lioz'Ω<sϭYHYNn H;3z۾id<ʄ$ގF$gr!RǹMO/W/-д_}|B^?<Ҕ<;MFk+cGeXބ䡭厥ˎ#[{Xͻ5[5Q|Bas1@3>^\ҨCCe2Mh&N[ۍk@ՁXKNQ:)ۭ/`t} 0廴geZ:"!SSJA+6`٤<=s,0Q0wQ88:';x:LL8~4X[-ȝ8|ݦ0z\ bA?Sۼdfk"l (he8%9l mN( c @IDATx69+FcD=hi ċSz[!9\·Z8P#s!2p m8h$f"n!Z8g2z&D`Jtu ҄M8lz^IT.%;;z&mNIRQ?,Q.f·r.w((`Ap$lpz( < ?#p]X+!,B 鿦 Kb\Βa,7Ey_E9lrs JT d/[-@igibrJ;j-rJlhS 4n}@#Embz;׹gќ^v_<}8fyv!R/[-HB>p]:,<[a8ڵ-M6UDS I%+& p$NėҐu)ϥ(6\Quھ6\yw΅1,6رt:lؑÇo;S#a.53l 옲HR6e@Zc_[ڎNK/<:4~]Zv8aȠ͉[~ǶcDag@B! 8iˁ m )%8ӹ-iF÷2_{LL(ݶWdQJ[ >-P 3'z%1IVOxd9ʁ ЄKiXTJI.sNA;\Y+iwv ӜE?QʅٲmKn)Ϗ6X/y`%ݼdu$ZXWwݼhaj`'@yҶl-<8˵j|{TUiyFO(n]@z-tq[!5EfhP aj߽b v7 cDvQ8`*7ZnиQzɮs"=A7~{:Ey27+rXeʚ:nbev3}6$[Gȳx:[ @.sR:/a v灦,Φ1Md^TTm*^(vd.>.E4/DOWϕǂ`W-HQ?QL :aeؘ.-% 9wh^.-v =ۨ'yy'&v95 ׃D2P#Dz#9\ <7oe&־;mBL;N@ON{hkggIu;㧿S>C zΝՌbQΏH0*>HɊŷ|L }0[8֒Mnwme~_Fꇲ8Gr>PvbO:bl>ٳOJT2# nɍt~H:AߗEl]*ؿXQM @>iBWf_OOgy]fG$1 ZpKASJnZdo;. hΦ0_/&V .yj$\Q1<&ちN5#;nJ+_*㉓< XܚRQ8&g~摥[4SgXu@!1xW 0ᮙa*qWF "BS6;JyV0ue]o7 hSF|KԄu~:][-hi.DtIQ;GYepE> ()BH[Mœ8[w/=P.DdMt^ *oXn=ɨʨ''S4IFJ "{;Chqq9~T=Ay0kV¡zagR-˶>/vPrd1FޕWeqYs9VI }ceJ usQ~+Һ.Kzi)Q'i!5+-OZ,"(J?{&YrFjof{vvaX,]\@Y4GЀ>}x"ԧӑ#uW)+'JD) n:6de>+zkߐ+l>жt۷6,oiđxչ6:'"&ҲW$tI'42MϏHp:rgzxZp$s4[miNnhI+>!#hzYip<Ӻd!za k6IDvL 7D 7N&W* (Ӱ}lKN7xA|<8ՒA#nxqK'x,eqq C>64@t|L4 6ꐮ%0x@`ꤳ~t&}aۗ: <1;䊬v,&y&W&߾r,h xoSjT:7 Be$i F:KD<{9Sx*wosL.G_I͡EyRg?+g$ 8Vᙝ}nulAyWp!fS2}{!CjNu@ȹ50~‚Wˍ/%/-7vД Vcwe|hPg3M;ߘ`kATMk-ˇ<ӫg1x^7U}W>3XX~IuTd/U;\T k>Ά[O_D 9 rQnjzeSTgMv;uY emܖ%,t?^F^Yf6}b4@fH -+_a"c2e1X4a2Ls/^3)O8"{Y:'Y䃙N'еmq[tߏۼ-/#=v "ޡ}ףվljN{j8eJ?tEO(YN҉ǿ Y]}|X8m֧CߚO~U^?rGd{6M<2< z']ߎr׾{5GkuҦuٝ6ޑe{Z:tlK +;WD[֗%eǾRtR:&gA.)ݔ!yǞ>%ceݾwKCN7x襧'w)ٰҾK w/q1Z\SXY޵cq0m& ˷4okiauS^fːMn,$-,j`ffk(q,Q򚣓j^_|禃M ]uؑnw뒸M mryl@z5pM})o"Xj@Fuz/xwܶߓz߭ tFD/2A7FHzNbP?Sru}l.\}M>"nC/Oq*:|uƙ'W>ii;M *ړ[=/8:ާ~,G3ߗnd*.i2ߨmz*|I~k%exZ/g [JsY4cDݤ6Ц64"C'ʓ*ֶ}24xB_s"g}u$]\!𤋮q]u{>|iwR>',t:5Ԣ [ 0 B/(0WG >麫kwv]ϧt묥ixN D8.q}92Fc%Pإ#BG)լ6lΡO|~dZo >8fKvcҫr{$RwD}y%:;~yߗ^ 2A;wN%tY{0oJ?OS0OM(fY\riG:J4$W.> "_sC˷<$^(';H) 4 vX9S0}Mû$Ȯu:A_~Iz[֦yݭocg9/wݢn=l\&'N4ӻU~d07lsֵWZWǟt1sziP8)++wKFic<g9,T[{W4] :N6nZnN֝W甼u6w=([WΜ{:ՠ/r7Yj8 [-/>22tڍlٲQ<#Y hpNUݾ[/a4}꫺m@7%W_9l+CwE'jS( gMIiuĈ_xN:wu8v,8{E;q3x(%fډw"ik\j/\ﱯђ]h:+iɾ%_*6nZSGi!Ze89kM eTOtp*>-I×.;<χ'{N iئ:$G fu[Y_?|s}LnէD ,IGwo|_{L:A001*_ɯCr=r>ӧ]Iz_|p{їu|LgN%O zA}1w^d _ݝQ'fA9qDkoos~9^i;?vɅ t3z,6&9,q_W; qښl sE#'^]KOGe;{nC} tLqTi.Pܝ8y49A}Pr2ۃm\vӾ 83xc  t,qz˜{\Ӹi9؇Kr0N10p lڸOedqu WX>y6+P_I_Q[vetG&x\<ur׏;'W>.|C[%ٮOG_|u f[^}?KuTc^ 7lγUjA[ܹss7zzz/q pnM>קN~>ߴ~R,tsiÐnxd絑3<(í49c\bH·-w˥?#i n!]}]ː{#oj09I6<U-œkX|ŠOfv Ɯ(fi4WӍ wUsxE!d 1Y,'&NEI>+ٯ؈[Lv/|:'7nT'dRߍei {ɷϿ&rf=rNEy}ɺRցh9i!ن r}|uomtyYhw&<>,z/?+?!t>3* u<ʆ".i*<}4}g_ǟK^/UȘu2ՙ T|tM,8$.Q6&D; !8NS]?waw6u.6LtL#xl NeAnpޔs\@FU_1 U]s^'0N*>bE\{A KJpwVBơ&]xo`2 9=BE\gXvb3,`#/}łX?kZع[wyicku򁆂9+=<AP^7x;mce3p۶#M0CvgЮ| %38p|R\!ḃn(7A.L[0,oo ժe3E*qcCF?Y[.{d!]s'sOz.KwSw>)'ԙŒ 9/&әIO <ʼn &=Cc r8u9)K)_9ܵigGMvsO2]b ˅\YS$gT3 qҒzhւsja"8OV{ĝ@pE_ E<ԉ3.N (PMx1gb6dq8˘ز3svm1v\1ø(Wp88SWt8w%!s^/7pA;™_եv >)9yu^MsztӦ-˶TjT.4Io*꼱A|w--uU)m'!i05P09gk(+Af19b" _ٷ:4=Gi^W-l\U`v&ÁqezxBe9 GR~0yu`.:6.ARR7`e;v1Y&GK>_XaڃoEwju5F{R(&Rg|)#zX;F_8\`5E)N`1IA5h>y}eÃ[W.ȗ[.PqkT_Z~Nd)O}I= iOE>@Z巈e뙻v)w# Ӿn*7|HϺ SOx@- ;-ב?iW>q`G<qA+lx>:Y钹I)%hr{` f/FҠwϝG'&m/noc{-?Ҧtc_,:A\v\ wC]yH\_yԊ>^ʱR8 șK >7^i){4ON7OwCڷ# e_ԭ{ŧe[?ϡ)laoζ<ڰ!{ScҺvǾ8'^1'lO ov)ˡq߮iaB}XbD4P Ԅvv*p"҉ !{G?JOCG:B;?y1S=yN'e|ֽK|HrQim@Կ_'[6:~;>U K 1u&ʈp^cIsvIn(QCwZJZqzI_`cA$ !ܵ']֦.g(6K@(|# bVg?~W*4㢂ur>+w@y_XRV^s+x! 1u ^Ԫ'^NڃboZmgP2Wn(/95j XZE ]-0wN7>%^:XycԖt;hH-L._-\jQw!q*.l!0yM듬z NNq?yiwڂyQ$]zҵ]%q8_qPԄ_Jw[7Te_RKc1l8pG/>1Q뷓zOqye໌z㨥K?29A1;BQ;Gqcִ"Aj|V1 @fc=M>!ӥ)útp_{4JXXS pAc'bC*ҊV."ZV 6Y0n! y}}䍏)E'.N<+_zt+.osd 'h>>Q;'~\zNˁ8m~v˞@fo;<{cLJl|".S+(S >L iPazz@J?@ltv}JsٽZ>-G_s| Bi&R4!{HߛS_-{$jrtJمq̜9n9_\?na8؀4gJwo_rFj]H c\ʥհ "ѫHk(A0t!Эk+l}L85jK:IkR} ?Js2! BE]Mr)o3 RޘtAXWO1/,/m[I$>Vz1 }Fw8Y!F0JŻW׾lUxTG+[}X^6v'd/g^/n&b̕'~C$CwiFLJz~.@q1qr폿h`އHqlmA[HsN>˧/c%qax;&uO:,LmPGi]4t66"Wmmal*B):mItF7Gv_V 6FG kxapjwI+u2,"v.,+OKT ':"c׎e|p6 j'B|:𮜱@j\է_q.OŸ\QIMxUR37zO~͛tp䠞lǢNs["Y8Xk| u'{WJHma"؀i'Mˁz TOmR{ Gijۢ֒%T 3Nj܄͚(&°(ݙCuչK&`i]!$+ E@9SOq寻[M0wr _[ %7: CPfu@hKlrtbfxTj;C O1ND?.8#٫}ZFo\YQB,O7xށ+3kAGyL)!Ys~NNPuy. '+9v:YO2 iƃFSƴV j  dN00X7] 䁜s2.u( 5}؅$[tWXyҥ01|l@ SIG^̗p<\Pnb 9:7XB)-weCt+P6gr4 @0v +EpP9n*%= ֒FU(I0CGvwwj"MKn,a idtR+[+X̀r:AB4v>r[+eHV 0>'K;*jT蓐=pR&a|mao NZ%a<zmfqg!_vN%3Â!r1L yv.gdG`sQ&p0(h4Wz=p>-T5wR|=ABY> %[(\d@~8Ii]~87|A[M:^`_"ӡees/_JWL:Jh5;3ҠwF%"_4^ʘumyՃrzM3rp:r@+\rpPa|l=ߗ >h26pKol(t'o׮_ }to@;^wtq!E#5\yDlI*@ t_W듩Vap\hx3)I,lLU1uRmJ/k^fd!Sa^$ymfE,rpW:On/mediJȑ+`%[mعoJe$6I\SNrn{bԡ<9:$pFh3l`{;JC>n|(ys!|` p8GIe(KPwq< ǏM}-7>Up|>?zRΗVZ6Op d?D~30̮gG{-+W4u]q 3yST^TmI6PF@(oK1>9;d!бlp4'9pv`1)dry%x^K!y4ym3K\t p-h 88]KǓ,u\862:Jh"2։:> }Ri\q]LGy\+0k/;Z+'D_ny s'=ic2-s5P`v4hLoi9JsrYiRRO01pi .RE0@j@NleL[ܶ[|6%*R0Eԅ\}q…_mO5 G}ua9FA  B3xĝC[ulP:rp qvCzz5V9IF Qӹ"3r904+KYq}cQHcK',OУh*%Ag?è7 Volr ia@Y^a%-" Ԝ.LwB6f)q|ȋ8aR'{dj-@"u[Q3!Zܯi9OY˱4L}ZC=0ȷF:y:!~:tǍ/l|C=^8(_. MVa8i vs#k(#m'L=y{Qy@IDAT7H24pP?14ݏj@l]xrG# o+%ٓ[| EREt[Sh\g[y>B.N^$>AB;˅ZH=SSrQ7 %ydC904R8'$;$aJlh\b)ڤKM.ڣ:̤Bfu @V_w]L ˷jqQR^?x[wcCgeXi=A'zC/.&< G2i5'78`JY7Ɔ6SC)]-O߫sf#tϗ? h.|HL[/e=@h`2,x~j& ivl](8'/a7i50x8Neǁf:(G6>/<"ol؊8ui$c,YiN NDݭqVqh|ljG/H뒻mvIݏsXK c!֜dI4P3hOT'Ή5\ZlضGVIfnf@<. 5guazmT?S:_梧Uu 40Җ h!i~ܖ .I󝃢〧HO_R㷏JkH~rj|}POS3^O%*7xqdDH:ᆍ#{* ܔfwAwA=i5K ~YmH:sNo8q}66_<-}*% ?c{c:$y]|1=SV@[Jˑ7xl|,rzߨh`8hcB2,N|b߇pxdIǺ3H?/xzf$4h3^i@U:J,6ΓOT>Vza p[Y>Ը_zn-nu !_ôMwWGU@gм'~kVRtYbX'LLӻ}7pQbV'ifgPʇ'ulU ?x/BX:z=GyV]s0V8,$-^GI|^2}^brl*-u_9Vǰ2Y:GOHKD?p͞gn3YFM_Q}}:g P,[V4H;t]>p<2~:|4yŒ.sM7c5KQ}+cHeC[Ji<,6DU"Xew֖!4‥u:F{ U)HO⽥{VFg=YBz@^C9JAbIp ~NW$9)'z}xyYh&ͩ5RG|" ֥tcͷa?H#|°qnK٦7INKCIZ?'&z&nM'?-GTzL[cGXs $vS #n?$[/:˛Ͻ CY=Q_VPHTdOV(HIa4?|qx8  F v0-NIR,X͇fW MG2i`M4Ǥ/yUK^У y/z$}&ڨJG/Jnl%BjҤJGoJJdAѦgIjJ?84@!,nI2!i`E4В'dG])ℨ8`X:sxN4ccc u>z [0,kf81uK>2 o1>QqMD3lzf!q{a_ıݗ^vHHKW_uFqBsMGN.hsXAH9x><%c=EOG7ɻK2YI>;O_I}.iq HUL|y6 jKA!ȷ_?\`!T[?+^˪.iя'T'zS)$QV7}}J*:,o,# lij40[/vU`}}}9gʹsHgx9yC$r'Iq[$;/WJ:K^YtBkV+Ԟd@ȓ@x2٨O~L >>MZ,Kt+wtvz+ I_, +|GNq}* MfM9a׸}É|{ t̃$>-3} `Fk1;q Zj(qIlNA֏ޞ^/zC,˚[3;PZכ /di@M[noA+wlk}X^ZNQH@"(338㩔=C;,΁>_ȇ~L!Iv7dz긟\iIJ,qĒwC5or^Zpũ1^I#`G2_H4pbs<3:;;ם}ol'^s} ٻw Tذ=ܓ$/HDa^ ǧ[Ge臃Ԙ_G̃9iEXU*w;P|vɻ S28=lU T/oIvC>ڄ(urGMc~ ;VC}V?DC?ٖ+16fDQBR䡼2_{Eb~Q+AϜW/M\ߠ4[?)BV+xM8J쌰NF>J;xiܕ(oR,֔Sܕ HG"6DO8+qyc~UK#NHzqPR Β?az[6y~kyiW&ef=jt.Ll.+AnM>h#Ѓz ]dmD^WQH55IWvfKprvkfs[\ɀxX6 ʧԣZ|o4M v q`9ϱ ^özbY㨇}uBFI2ƻ+-N/50tHӤr<Tmx2X"CCSSS2::*tڥ;cG iu; u8pa`M9JP.;B_t<6^7L&kd.x>9ړhͩNiC催I 6q@{!1XJA`b=#Ow5ءRY6evO8hf!*q9?qEVK_RqF6OLYseޑpvn۝n}ݳO ?0Nq XqP=A}:-]]GH+I}[@j h38hv]rE7@ǦOv6l ;wt8N~hL^8lQXbg;~!E4ā.5)VsR9cQegf.9x^SrVw˻܈ kүvQqؚh m7YkeΉ it-:Fj>"NJAswۏʵW o5tQ7Nw{Cvqid∳->$ov#stҼ|oa68EXʱ^ vQ`C+;q @[Zۊ4‬!-Ҹ_uoe tt |D" ̫|7i8}NwxK}{ge(6nHu}Ilwoǎn^J=sZbSS2~];J$B%K~\V&%vDT'$H(v.(CO4o={ qOɵ 'u3IIN/*X\tkXRu&=3PĉAE- g> ke"!PF zG8)Q""Fؒ}dҴ TNc~:uGDz;1}/A9@}ZYp>__Qワ6KwHj4NXMnp `8ƛwM9Bm6Nt@?`ܹS^u;ı$Iʖi6tIF޼G5 qF~jQnl u͏$8 YqA6HXa AaHQ֦Y^ 1X $ thwG>#SbkF@Kb8K@أt'7W+.~Ґr<"?"?˕7 _SywaB'Qa0: h+ thS^y1xdͻŭuExh` S&tCxa.yV9- bC[[u]LKG^oN?B4ڐ2^4 VG\mn@ L+nr(p [:p%WHÄyo']*IdL.2nBկ*v)tR"/O[ 8i!r? oͧ'dq Z }3ȃ}hoGhQ73݅v8A!:!+ Q'dcUΛ #Ki!ȃ<4 qB\b!jA!G@t vPׄ# I~;F?5+;dZ?&yX4p86hn~:(k B< G9$u>#nIBԖ] %(J8i:ž@7섏NFް>2Bq L}4BApKr]b@N蕲u<;v̗ un1=ZM ݇Tk(g14;g,)+ DCyA"n!Rim^qi6D#e!8S۳Q42m9Nl?hB*I1㤥)"J[җ%6<.- uN>X.%(PCa@"מhr4l!8h>,>Z"g 6.^95y` j .ޯ RKE.v]A! rRyK͛}.x1r}bP-˸>ZNȾ"dF8ۤuŵ=N5׍zK?#! GB5^ɜ[7ҷ=p!D8Q!lTpB }%~<-'t' 9|%eDmB'ck;o8/ tIW꠭ZH[F1GLx\4XCBG|eY@L#}  \$!f'I:2߸Z+DztTu$$ΟS_Lk}QI,MwȜћS oS5pw*;G z;$3l[Z "Mc< V.XxNZD>0hi% -pl#O쏄~n+䦣_6>X1'|k !/Oʕ\acT ϱW FZm,q,>x52g9O"N>k~l.veYce wyB$mB4BaDŽZL.u0sqPnbEN:CW3KG gα8;lڸD`x\q }:aemy20jzlO:r?M;?8IdvC*mjcqSv.=BЉZ0Z`e!a0˳~gҫB>藐rQn@vx.^ghBcY 3ahVa0~3q #<~!{+;Jϓ5=()]FFՄoD$-mu18 Ű.A-$-]!%j'ā#$"!?,Cf4@-⬟4'X^,ƗjhC~apG@> AK&rkow:H3]]]BV8e|x6txjSxJQܶ҈ݧU.n(G g3m8; qv"!X6Ʋ6iL'a4 (kB@؋zgky[$_ $cB牟Ǔ6-\ Wy>s^$4 Ƀy2~X?〖FXx-}Nh"GGƥE.%$Dg4@3@+|ǯnwlJIͤ&2'|mo) =H5]p;[3݇Hyf-FczX}X_ );hz籮f5 YD0@I ` ?#d[ٓ=ۓ=qT@[(?6'&>RyJ΅(o:j!Nm#qqQ䡾e?D?i`J˳$ /I{aJюN0>:ry$-_<ą> [o:ؤL77b-v}ܰ;-;Ҙ/ia: 8X\ֲ 1Ҕl)1NvK%\v| l ~͠yJ|iZp~q#-v!;%HU@^/ϩK|/ԜI{Њ4BGiฟm:Vw*SIƞIa tR~_HayB% u2y<aչT (YN14|c<hH 2_B&&8jk/[6i-J{%:ANH-ZXd^ly-aӁ< I+Yq #df).o@{\kz$%aF(jp'6Ki9]GE a>X:zL}E2?~Q>py\GJmU.Kqtͤ3CPp3g|%`Lu P+%C%#r´4ۑdK+QlyYTRtG6C:LNj7@|@+}"QUQˆ}~֬i~z=m9,m!m]l~,W\,faM;gBW)]C |zR 'wq~`Okw!pℤ9X<6>5_e:wNiUTO{4-?( }?Er_IsuL>sb{%Kb- 2=OgԋD6I}"ɸL$*O&{Pf5$wL>sBb O2X^v DNЉ=,m}O(-uPO[gYI 2:As؝Rl*:>*/gRe=vm7euw8ܱvZۚ8Al+{Fy'Di}J%IO"~k'v'?Ga8hB!RʒLIcqb{wv{NW~_uUwU;2˗Ǘke9 k l"9}d׬@ $N; &e;q1wP$3n I?.G{~_P]~Y"~[mׅ둵&wפ%+c,-0W&+ǀsK/Ǫq ry%t['BK/D}3*T.IQ.V -jgD&-S,|r1]u'7_?S)cQ\H$ܛsٔta) 2\?I>z}BU=I6!fi*0s4K`@'-~XoD巕gݹ/Y|؆Ɇ"e%$;G[Cg# TJzPHNC(+~ǽ2OIiqY_~KrS2Gt{%Rf_X>AJ̯|*W,HOD8~SsCP.hn[*a$8J6fwadkQPN~>6nU5>,oHsey뭷dhhHN:%lV 2[8oE|[`rQ_A[> }wY!>`Ͳ_#ZP~'KHM -곏s&2OQd474Fv;kd&}6_r7O2qB U K]]Fe铔{mnA)LW"V\'w/П7.wsQub[2e)2ݒ=G祉g8d 1W/7322dYWjw({R;5^QZzEQ/ko$-ĥʀ>.ǣkA;"-iBN֣D&yLT3rkⷩtӁu$ هl>y4 hB , ؾBۗ 9#s2:Y EIjNڟ}'/fnaDd={F= xH2GFQ2=cW*-qsϟICrST;3p r_T-|XWG%_R}dFtBC ա^6#O}7fr[&fqTWQNv6pCh+8ĝ9sF\"rwG3wᕹOk1+& p{Z O O dVZ0Z͟}5uL~e;OB;O?8gΓZzNF/nVBm@NzRݒOIkys%h΋QuI迅BA߿$߾uGcGa{\^ry][t9]GDI/j.Wt`"%LRʀ{%ws&s9wJuݙokEQ6-)N%}/)^JJV%+ oq~I:>~@%NǍw l@6XڇG )ϥw۹A#/g^cz B"ywy?^Y$s`ZH{+G"^Q߆J쏜>M#YB/zM4! l5 yοs#/ /ɹН+&*98XPdzB]:z+Vq*c̎(5}ܲ~))A8~*:PoלS{͹ҷ$%}*ќMzHmm؀M am4p-K=HL+6\p!z oBAI7f,7%قJ=L]sx|78og/]z $-7_$%#d_S&W46r\O="I]\cA8b/kW݈ȆV=B gEc9#) s :nQV$bg$MIx ].ӯM -PT nlz!YĻ寗ί!mէjQaiZy@xc듘2'WnZ[V~z>= a:Cvzg:𠇅y3i'YDRֵkעġ {I ՗=(3=h֐nKX )]NЍ7zoBU['X`[7?7r5oWLv,@j3;/΍'dZB֐ž+ 1GiG`pNͦ)5(AGԅyW_A[܊TqV:B?8Ip"GJVHە-87VZ.v"ЫzD]\K2>GTwCa=ld0/=@wQ5㝤Z0AtD92pR.{K6e-bLbXwgny787zCp/T}p ,kỤv^EwăvY[z/dO|k' ʕSH)=n~|Α/k\?']D>^]n%C*֦c+2@GcŖԶ3r̪Pe"RtnSz?{zjI8K\(u*I[>Ғ|Ǧя6O^WyD*g#կLW'/@{N.zz2->Ms1YT_˿ʷ7ߍr,Sҟ|M-d#r[׏V[${syGܽPe-dJ@] Ϻ(ZdD݁ "(i9patd铥ˎ(T^!_aJ %Y/DqQigb(-, ppnA̠"q^L?O19%_o]\ڔ-z?<4(}U<8%MBFii8n]):H_ľ=~C25 -=?qS^O G;O X6@IDATHC[ |V;# =qLpۋ|@>!usY(V&uH=Gv}D\远OQgΙiM`Ie>\ONxDCmJ`BNEODn\yOj?8VgeYÃR-x:k 4[)z2'H @+qLNOX&nv[IEBkTS[yܓE׃({.}āe!&^hNCZc~ҀXn@z^#=uLO]]|Z,:#Nu f{H׏`x ^˳-D@3XЏ9:`<򐇴y1/i|\уݥKFeHlxLrg<s%ӡM[h8Gmaw I6~'KjU2pUd#Q ,8&Z.؉[waݟR,P#U^C$^|kAޮO`WWFEڭ3n8x`D"vy]=%ĭ ˽n{>]zPzDX@.3Hҩ6oq7zM]3,]-х/ 鮇d.}B枕p2^Z/|9O&wߕ8FJE{RF\[QrUaoO֎톺Nb&)D)z!P7IzR-Nyvtqpn2S֩n˲x M`B|T7|#ԩSdo&m<ɣ--59Ls ("[ !#C]X͎7q 7=i@߅j`q  |\ ɳо>gd_{]췕 ZrZn$mg]=s#0s%lz‾ 6?_J==&|B"c|h{7 +­j(m`g N/\Nc OS_5]LT&&&Ç:3@{--N'Zy4qpbEd4FܠRww~D?|70٢96Bc8y $ `c8!?9q\Z}_%de hR!YyoGd> j# Z3(_zCzMJ G-p%` s/+hV܀ tv8&.^޶OX_ w}zBu%xbD/k;#i76St-yHO__&s//t-0[f#}Q{rrtqAk?"p`Ny3%osp$m!ғ^H /=RVvI;RtL+pd>9"N|-hӲC,4+v[S[/s08J-l.YoTam Y$hƓ]`p6x I7П7z} 3~JߵzCˇMbLS… DYp @ڎ4q_<ʢ,BʶߗzXF.}MOȻ+.n~(8u:HEvF2M"4#fO<`n;ի/r υԝI׭ÅWtK^R=3NO佒OFsu|<_ˡHl8i l.~#.{ @ɳ#x͏nq@wi7}ּ=2?膳T~Yd:q\'\A;]Γ<;?4y;7k(Ɓ3n/Jm\oZ.|fqvڝ;'Qb"pV q'I I%Oz2't#D~BBv < IZi-⌧BqYgazIy$e(} GcڐEhq°YQd?ٗ4aWklwiU\yaY8>RIDXJv R;ȶQvM$/$sa0VarN$ |c q-x\eJMgo*|SOn? IIovHғm7y n<<.(A s 95Ϝ9#ccc|h2Z\J}4*kҧ'vGȩv%/e'\0m/wa[ٳg#i,/-ÇC-t(? q}  .:I9W%ҝ;,n~_\~da]f/; `9t8Y;A ID_\-uWx^aG€tmd[hke~Gt e{=++lފN[$g$Ӣҗ<.w`8/ԛ87 V.p[>?'`@+gǍ<Ε OID=H.,{+{Z_k}Pѿ`[v z;(u0q1& SzvvAD%0Ha?ސmp;H6"2nYoxE>]nbvOu|,Pe8&qb|k*7zun-ևGg8H,tq!  Է'``r>D9L?33k(-d0dtM~du/sSt.MHWyRzxWyFi}~әzR^zeYte;9'`h`x6. H]b^@xhXm}~}h#syZ23[[SIa$ؓm1Y# 'o(ue08AWf[B!-m݊xJGi݁Ɖ9mwmڤ:S^Oéz8M/_By^E)ZR9[zr}࿶\ץ=+LrXQ&N[J&'"6\!æqQ}8y  m/Mo%0c!!=O?^Jʢ>t\.VG2YZΏil'sdJStL:jY6 i:ΟD*Oʉ2tцwq={$.6COmZqi2Irೋp'mp}|<7K2Q[CXyy$y/UN'QΗǣLeA -nwӸG8yr`vgwaˣ[[+%?p\O&9!4v_M\'J j vqd-g!8vC~+<64:3SK-|N֐CutiL*y'$qmlq<^q78>S& iBh8\|BF/F#͕H2i@˯p*6# 6XoW&|-o;>+hhυJv|^/LnuTDz.<a8ykA@D'(DM ZZoE1qēW^y%=D'1ޗqΕH@y>9]XG L`z 'xk\-tq :W8>@}<Qo@@,9 4prIq;vlhyy1鵠/APFGMGyGd]x=hۋ2,χ^SSSQ^ؑl~>dov:lƾ~zu{؏?#v5EKqzXn޷o'IqNmn񵍍xp::>8{c ,-a=J7^̸ t=ed<9yQ6q 3!d}4x6>NBwӑ&D9 -χg!p!X X`-ȹ4qhm< inǹ鈻,M-8ʫl]Ae&tGiݺxH4 o' mj%+w菞C_8+*x԰?t6.٧8N]G[p0z׆j!n-ĩ+8|yj Z׃ ,4ac2oFyn>|<SZɳ|8 XN [cC>Pnv78 | xSs'QM Ç+Al:KG`|l37]wœ@LJxVw7="㛈+q]EKk,[\xA9Ǡ~-8Inzx~~A QP+ov4q_ 3;Iia8ڗ-n3Fp`v$=6n rZI4RW℔,D~P<N6Cqz1- ż499)7oތwZF:"|mZ6 p#BWg['B[׋iqrtF #PFڙ#ڋdB7m;I8nNQδﲆbXv`X-wÖuA=S$0Vn3e!<.U( SXqB!Qhe/``[r7>,K/Eޱyʎ|髻#MhmFz!e-S%f|%jGYBC=:Gح=rioeZ*o8v^sD:dZȇæ6 08J-lE.@B1nR -_݅E9u>?D'Pre|G:1- 㙟|nh7ؼ![z-|xaĕI~-`՗rx_l;x pkX^Ԭ^4m?pe#naa8y4# h0Mϴ"XUoiA`#|185vG|]G<6ԋŭޮlרC9l[.8ɖ<wvB=?m.)5})?՟Ώup].؏v#6nxpbnc7\6~Qo1Hiw 7cL@8x<7MkqgZ.]6dH_/΍ovn_B%mi[ZC|{,`Öx6Fqb1wx3h,mA3ŭlg^,wyÑ^wٳg1>W'Njuע""1VX}F|^?2 {^sXڳO̖zr>CQOs;9K(mEHvx&' ѓqSx Kh'画9 f jO!Cqqͳ^wS>_<}|fk+Ґ5SnH۸|m֢Q&N_ۺhvӸQ-YWFxn>W yn[}hs2փS k4NZ<.gdAfwd\MP)SJNqm,tEZݞi(Ahq҄ ,mmnq ^ ڣTu+9*@>ӣ }m:_|<ۆhgqJgm+<řO61$գm\jtn#gR|/K)屼(As ߅Ǵ)/Q^\~,/lfk6FZ'by&%yO2 B8<xz&, ;鷇d |kJR{i|i +7O At4;mz0ټ mSBpؒvǵ3=XP[O@0 . o fWKwou|ƹ<7ד8\ϟl?ئ!Nyq[[{nm>۴iփCheZ'NO>]mr2piFOIRǟ2 _nE=~. ☟|v0"vOpԈCWm*z׋}}iYVrc@16q*ՉNHۭ<7'/B|2ɳmGДABEZM ^k[ '(y(7綛}8K.Eb ,..FO=XPOwiu![~ymv>rIa'=776|q52M,Qn><ȶЖA>xO]Hq2<8v`"]#,u'dNacDR 9<+{cA<\(H) 8%1ge9@t mϽձ[fJP%鹹(]SK5,]s'|vy #Kzb^{z$ߗtt^ԕhxNF; R Z؊ryq ڂQ]Ao]ߪx3:kf%+7g>><8%lY7Rgײ&)ZNu B8ѭ,Aګ//QG]eO{/# թEApdL-JꦶMg6:NҰ=zоuP+I w8DũwVʹ4ihf e5ǥmFVF.]w^w244T nUZudlIQR7skR)';9uk,!6tqBNL(lT:Kˇ :DyQ'A?UmZ.Hz!/RuJ:&a( uJzj+/ʡNP\|۩8sqҀıX^^mm\`W+hJ))‰>mYy6cz8㭞eyklbpaCĹcg! xnu%xtO8ܗr,~,NNRZytp"93eRVɳ~U*ݩNb/oik'ԷSv A,4`>z]7uq:.Ȇ/9GU^2*KDQ2u}\4aLa6g\B#E9AeIBq"8g2O?v RZGuy <_>Еm R{ #v#VmVV{Ta 'X` ~K/K^HKzz֐3Z^[w$n%ZDsA]KPw8h{%.֞A:NɊ(9]+rfIU^ޕŚT <oq;)n -tqJ\_jˎVO2g磫ԛRxP'=.m[#'aylB9m zفԉNi :Ġ40"b~Iw\p ]<܁?Q@rpéYGpםk~d<DGZ*E;bU&Ç>W䢞jޚ\+!WIRCdRѡg dB9|9`@',`%q ]4rY~|'t9AF/z>ez\0 3gcuشD[emx:CLKH96C&ygNKDY(ʷ4u08Jmh]->fC_^o߻G+o|Z^i= `'^ڑu0c0ɭ.נGڿO6z].TO&J \8bf" uVX7LsK9I#p8 X~]%}~,=Sʍs=:/>e:O黀' }*'i}cnҾzsLZ⾼;28XuuPՕ`B_z,YuVqbu}g8nGz08Q IJqÄc]?3D6թ~К|4%lř2]<[\उ7G9۲6;@vdHtwCSg=#i+WƲRL|mrDOӧL=:*c"'Bo*-BO-~kqw_W/wE>{ۥK7t9r>zH_\*˸AG!=Kׯc##r\(~OK>OnrVXqU#^u.-,T֬Jl0E$3_u1!, G8w.!_'H3~'7W.$Qo%@^2OuaǕ1J: xkI:/W8Cn!>ä2-K,/WC<.Vz۝AN}}LzY˴n=FtxwnhgqƦA:kqɧLq@'o=2q ]nQ`+3RcݮBV_~'a\UzRY'Ap~Q|OTga%z$n{_[z4o=:=#O쑏jziyhQC?(?}V糺MoQQu۳GީOw>q-9ilV|@ޣ2TT[}H3Rw6Fym|z儸`f,~^Tw_݉_O~aTm|Q>7?'ۯ%m{ mʢ]y,ql汴G^'m!uAxAzЦsqҀCp:ꓗԛ㯩ÓҼz{dN}N_0=U_U'N|JӞ{awj O[`{# _!ŗ_hޏkRwy1Q';(NFF(\*z"ࠉ:hu"iY&=#6*+!tӂρoq7]ZaC^\ԧH_?VvN_JޜdHg+u:'>l`q7vQ.}M}5'}SB?N~ݾ,~q­vlgԅ8^i=i׵]l[:{~A,~@}BhAYcl#@Yь|k$P/< 8ACp-Eq :$$?AuFƽOu,.-:~\x~V*3sRPM&~XudOZY!=&j}qeDO|"M߼qFMME-nC8O\lMM_k7O*`ika )6/ `,"'E-~]9*a_ʒ/~rD1bN |I>[ѰR˓)Y>|@_>nŃt| aj͋j3΋qN^` D?I\ѭv 遧;5#~Sa'րўL8m%H}-r=$?v1yMOiȞ W4ԸG?_I Q|<'I 6lE' 0YOf_yb~>4+[%v~nۃף[#=Ϭ#:7l3K-@#H8#BqƓ7~ `07"4U<|ψ!zO'rqz|夻;N/_lI}7q ߐu<~{%O>m0Ew\=#OStOd=zI=eVw鶸/],zPa}AsGzT2Uf d =/񝄎#Tt<-wG~Eq8n Zeq7@%h$Im_:KD%+7,_d:I Ʉ |,?QREY' }6%Ny n8I_P'iߤӓy~쐶TtHǙo#,6 pt"!0{aU?KE._mHPwp\]DT="l;ЕՃrcqA "JTqdA I 85S*pv M~ú&ҲtBJY8^IVuv*NIRSE?=.T:%p!=^r/S})MK&XB h/Gd U <0`^_ȟsuvv [ȟ0d{~/n@@nNQV N-KsNRp6鉼>WeWShsڭo`yBAGrH֓kG[`ŧ 1kX23{=4cK3Nn:< =zGxg(Xh̀ɉ"狸p$Z ݬU5{KVG䇃:B57;F#} UAsDž :IwB!XtDn_'} !8w4;h+ҶF-n=|hKe,g0/I=g6 vvg\n=<^[ޯݔ[f;}|g+R/Φx<⬌[ 8JiIֲ8JP,DZ p4xhk[u$r0<ªQy SecEB ,ĨʨUg[OT.V-G%B:j9@9Sp46kQ m6+2z1L i]{cLgaM@\}Aۋ}+JgtLNKZ =dIrWrWɴ}v\xuvI }%Vi$R 2-mQjSuыK}DIʏ GMK>mXD| ) yj|ҫ-@4|n !X`,"m/, 3an)%<@IDATo쪅.ӹ!k2`sK3fCA܅om[o,7l1>d[<.|e,Ptڎ==ퟕUmbכ̈́z6fpnN /:3jH(U85AVD7.2 ToXG+BMm;8/7zq,@ Fg!pKGO t/7[`"-vKq x7t.zZ]:%K] 4A'}EdV֑6GqLJ!8 JH噦m%q2Ӈ}JѶ}m6m>|+sԁ u+:daY,4ZU224סcSӺ#$ h\]ǥwu]䅰/wKv}OheĘ=|ai8%u.#|y i=KJO%Y}Bakb/mv݉^z8nv̔,c$mc0b llw`Eӎzu;thjVWvޕQnF>,{BA!qq0~;E Bӗ{I4QPoI1_hmܱ$7fA@KzQglG7.}))jo_^5z_rmlڵQjE zBh֏ڦ{(|,Ь y-]}לiܬݚ~FxkB0&mfԋ4`ZQүwfhMO zl\v8ۡS`,4`ڢ_ [m 0䪶wkg-3]Rڛ ow{L?B{,n QqB#i֒wpE.}Kw:{'7^%`/|r&]͎%%s~-+_Slu}|ͷJ:!@p: vn;O>Bh+$m\6bQ~T}34uVznY޼ -b>yN&Vvԡ{E6^ H@VNVZI}vBYj(hU!(}tk Rv#,Ӥ FE)T !@9(8FH~Ţq X9l[_(x˳0ּ—[ak؊ߊ:@phs.Qq~=?FUvIҩ5Ob;Xc=Bh\T"]X<|pols(%:H+ԓE^ᔳV5Ef2)髫4~A3P-/~~*ٙɂ,f2.lpZȾŶ9r8-?D~4="U* mVPOPWIlc~xJlQ/OSl=Q!nH:W~>u2 G E8][OUGAzTm:E.mmv!lА!XGF\W7ÑZB/dW=%{b@l(Ф[vMq=AO ~kPB6R+r#z|I?vJ;SV-,ni 0XR]tsQ]h?*E* 1?ݵA45g,@ 8oazZ #c6mqƷ K0}>;`قl;mt+AB $theV?o!MbDv@x>[:ww BhC|4:Ga|J [hy.u6շbW3"6J&<,̕$uӚ5H۶im÷Zmk+(l^uT]y,x,(`9h[Cth"FK[iwi[TmE]ЖcqGݒceCh%>ӯm^S.xKCBhc.9bqj1.g(#:Ot, |nCmhg|nK r%Ү}A#|,I-MG)Z q#= e;`%2S[~;]dg5?u`oe\.&~Y)="P34~<wsQZ[ x?NmNׇiwa\\,ФxE6xK7ڤ)NXVݶhۍrnɛQjR'Ox2-nu2)e'o6"6mxb*ڛm6pEx7.лnbuLO{@am[Ҟ-w }ڂ%*XV F/{?t뻥)i 2<_# V[ =U9Ёmc!ۂSkw71yO>h'OfR5e7[FynYN4*e뤣]m=v~MRsG KjʴtrsOB偌ӵ1k=%'c )s!5ayPuIt9:elN|wmd,✸\[HgGCУnٟNo/im 4y! 4bP-^PIЈLC݇ޟ~KJ2'NOȨ-Ni%.++uzT g9ޢ~'wݚ4uړm Gn3nkj^*=Rԫ c-#—5/Y]e~â2_o-'6 M4Tۛ:JYSSr;ȩ8I]rDpp#Z ]a;X^@ 4߈֤.6 IWLXJ/I2cQ?Y!|X[/O|neK#<<G ًeg cTOZcݭs8m+TH_|Ѯqu8\ I} r]>Y}+WrK =Mb>ʱQ>m?OeR"hGC(ѓRIqc:IF飄SZe͇t[!G._"٘aDHW7iφbe݄<689VfwԦ6ǀ@$D1v$zD_U99-EDvit|\h%ֶؓ>F?`Zh-.KyPyB9j"}!m0&eޛ_.ek|QWU*vKi,cx( TR*D%ԪH-֙h˷xxwB+8Gc>G#I?'9uW ӧ@ PIձ*'9;\d^U[Y~kRlPOFDWmWn+|W/_))pQ6\qb(J֕-Em奂,gth$&_,Dr/uJ(_xNJ˘_/WWjuݘa n*r1xrG;&N9#OQPp!h\X3AkqrjɕnVD8a8mpRJVt4'OO:z[$hnzyATL$X?]R<(3O.1C}/D+}"}GR|}QگNtI~첔U8USԽ@VʙEzA۹XF ,Tia/_`0' Of H''H@ޘz2I/XaI/dEj)9]$խ-FcJۭ}ࠔuJkZ0SN21t=$ѣOKףvBҴ~:@z,1+5mTq?dsDjYRѾ1%7&+.А}@wH[QuIV\Z;fokHx)N@R G 1eWz؉-zcwBvp44e4~a6 `e g}E"223232ܪss9q߻^papA]a.m 7;+ޏ).$ Hq1<<1'G5y|*4'='ȧ ?`+de4h\Y|[1벢q-k49gR8ch}WrCc_@JM|FN-q}k^n3`)TП%y?#CwޕׯGw'>!'N_n޼3P255?/h_*W^%<ȈsbE O#<"KӫR!,/OJ:pFgU-`H+R~] V!ԕߣEPv-Z%w]g}s<)Յ5ڽyY?'COKϧIQyvDztQ-*_I8n`OŸ|%e^7xg4JSQ-qEi e/?HX̋gWB?**j0!`"夂ͼ@v7 ZfH]Z^] XG9Yw4zGgK_|S2" VJcetHџسG.sc\(a2bjnbޞq@GĖt&x𹴩e~mW1>5UeR}~@N~aH?'3L3!HZ]|Z._ӿ4 @]ԬfI}kA{AWg~uEL* \.K +YwR߿森zˬj /g "w ຀ ޽{@-uk +=!PZ]]I(^^^}>ԩS277WP<,C㏇dIC9LgTW+dɞեlaU 4H*^q[{e_:.ޘV7 d~ 2+ªSR|_W'wG)CpU6ձ$-,I:#2:W|jycM )5RDAY{W;Z/<>d]>e&-'ƥZTU -%6L@3? {o{9|/jL>l@ = ii7d @[{d?+FeV_F;+Nʏs!t4AN_ w%Ȩ τ!uv>d{dPkOب/>t?첞W3I]- 6KΓ(#צ"\)aZXDR[ǟA_c#r9Îs14@E},1x6oqŕ<@rNSDg[{Th5/NuVzO)]ދa,Ēɬ  zK Y-k  2\9O K(<_ n%^ݬhnjF8 Ԅ~Stzg'Os\gªV҄%(Putt4"!~?ʑG X.\ui $!s%Y~SQ[^ y^ ,tVV&d))ͬIn a^V"K[rRbrm##()_JV;!z~}A#OVKz;yXDWbŇCElZj=_w,'viYhy.aI٠|18 'O<T}&84%)5T1}FHߛU6He}& AK>Ɇ %~_}kV-wzΏt+%A `e̫.ɃɊ#(-9ᶭZDaoCVRD>,485KlRE7LBгZŽ !k Ngm! pMU}fUiF cLQ"OqFXupwCY8?pn3T򠷖yf48zCoVU}6U19y'?o k4 Xž #2m8XIT"IbѬF+rP?+`u^*k |Jho\|=xTy>Ht߬jFٳޖ{g{e;Ngڇ_%e%|!.?,}"eS48z6 88 ,e[P{- Z ^ON}~D{J4&6ZЭ4+z\|N_o/ Y>׳_ It7̠[%[}OĪ۵6S^}qF7(‡zZv՞:N۪/;.y[@}t'ʼJyONGdY9ۯϜ,dI/Nbjދ:)%}辱OӺmv$ xjTh[}P:/e[F^J6>i=|VF?6&cpQ3 jZ>Y'IulRfRW3Rx/1:%Ks~aŶ_Iѣ ]=d 9},u_?{IF@^?p^9#OKqkz9>an+SIM*5)gOJN{Kn,ѯ?ƙ 6ޚg☜ꮞ³Vaջ$PP@ 1(Ǟȝoʄ;>Am :tUv 8wYwe௜ᨬՁCUyL'oF@hK\!îajS$_֪ʝ5YDؤaDy?`[oD[p\AwhLMYilۻ!@4zDcqP1^r+v1K$NI"Hgnˎ+f3ZwtErdڄnQgχ{dQA U2E#=FdP8u 4U9] xA4ؚ;r/$52@OLN!8BprYց}f_$}WO鮘C5>ZzhػpJ7E}o?/}f ?}6(o\%Σr,AkOKsrKxKoxt'-ysdfx6sI$E#η l]̓1-<'G`;j%A)6_Ạ##W{rW&ߝ 16H}MIW*~N)U8|UkPpj>_rRL#~q |&BNTj;aNueI}[@kNcϏ`< 3%?sh1Ë(mq1<.ڸЂeBuۡ_䗴kkkŰ4SNS3zc D{}Qq&<0>EB^9m9ܧ7nm` 6vWwy[׊8G3I(1U=o~87jĜ-=:ĠC!BVH4uk XNAORJp>bUI0Amp9lX (/lpR B6 9'5hy\oCt ecL"w<>K=c]P~%lu"~G>>XF!ÁDJJFqkzg9W$CfpuiLWAƴDl*JiW_2۵Cն85zkȒ96΁v%PԱk VN8O%>獶 Zԣ8ϐnW[iy$ڛ1_G`#(mdO$p ,%~AYb{AjgY/(|ItF\ t, *>ƕ_GVCɃ@eq{m'GsoCAAoɽ?\t|)8.$HB:я~B9"!ԡޏ#V8;,*k֦UM](bըDO;_\i[nmL9(nl1%ۯNC֞mQG}l.-v:uj}|^{dxW]z`?@ +CyA P(g@m?@)=&(fI?N`sD,wl$(EAG/wLK6NVsR4E;bWb7ݑ-c Eϯ#Em6߼헠׭+FV7WS/I+{,r < Zk7Dz܎k륝h8NZeVY>mQ 6ChE|a ),EPri y ”?VG̓O PF,ʠx&S)8@_iy;FV.339m16mNGj́?C3y3;׬VSa <|on'u^g+=Y >͑ieN7a+˧+x/zl|oc\5!Z  |EkQ-8J&~>v1eqOv12OY3{kɃoU@.3>oAF`<0(C0otlR:ŷek(uvf(vfoVqvdH2#Nwy>I9!XJ](5F7!ۭ 1R7͹oS>KޞX]Z[YF9Gk|Hoe93eWlxLV' ȷJV7[9iǎE(cީ#g/7uii=WCȔlsN0En:BݠE s ꠧ"xV֕{ڀΎبz_ߛSn?ʜ6X<ȓl,Vğ9G9(5f_Jfxf`H%yИl'~m>9.tCdVv~{UsѬʋ5b268f2iWg*wgΏXϟ_ [>+P)oE٦-黝M8io+=z.G0,S!RڜP^lOUe(ouMZ|D(d<vOҘGޓ#eg3嬃jgd PmgmU:v`趻JM¼d۪X׮9wzܭ=5y$eݜezq]8^=st7D87[u>q[3o]yosu5Iokfw/>N#RpP#VaQr6~X|2i|2q2K#^ A}ltw<:b>&K} HsC #,_;~p(vSMv2nvҕ>Nϧ#8s¾?];d| u6KHѶY(>rHiy<9Y~,>9ݥ{tlIoc\_6i{I+̞+3>?_x,NpդhE^MHUYZf2kglUYO. sYn=Lg/?$ ]]^l)yiИl-yEb>m9uГ|>Jcͷֹu^OM\Hc^sHc{0vfKqSϷMxz}F[蚶a}vCVfYo,<۵ykuja-Q#RכhsyeMcj4b>- f㰞͓oEcwt8R;.\Ӛ K:G(ޤ]7YnՅ:X9gKRU#KRjHL-έ٪}J7j,/Ұ~oM("{"l8ly3~%d̩#֞;.rwڪm,?k"/W\<ǫ5gnC?+Uǭ_/ˤnwE{mG츩|^f_wmb>Ts ߚaB'G"Ei+ !ƶidgrʜ:{N[%ʋsR-[z`v ϲz4ˏVdE_}<;'۬[LyԖGJ?r5hGGVsnnv'o˛c+zݣJGiL;fn3;u"8xn#O_Iq3ltڕ:tz(1tw#Y=Ss#mӟ/J73+gK x4.Q6Vn1srVέ_M"i69@mq 7`fuby\\'G0"`mi?7Ҍ|i0B9UeYuzq PNŐ߯D]6 _Z/5̸CEIhFፉF~ڌVt-xauH ˻6{fv\i>NW>3)vݧމ;"C[w&SԎGc<)MC{ܟWjf>vӳI|ڼ?dL?) 4TPUΉv],x[@hJ͐q#8Mm&|3[4+5wI)[ kAhԤ=oSEY6 AOJi4.-Ǹ&o.9׍y M_ȫzM&oP\iGFYJ(ώW ;`<5X=;'+ONnpx,~ė2dVeo9CHy2Y)ﭲ]>ݧd\A ;e`nLq ^Rl>'_Rfmr,ZD|i\cGek|E]wj|f+~zJ#` &];%]\ ]=]pz͊P?Ko/+,AGdѕ:F\[,9<0@IDAT)yMJ,}ya<6n@,e'G`3

[<8p^j2~t6ܐ|WIp".okW!2Y;8@--4/w>,?wOw;Ϥ,_ܒlOv;8=H:I1A6z݇><]#G8 _X1hbUY>yK aui@:åUYxTN*Rcg;:<"O:C ɓZolU mO<0'u0'_\kr=X_ؼd"]~2!=X5t#/'(mUi1',ڷ9Iv@iyKGp6 @Gt~á<K7s2wqOwgi)Y}xZW ' :9̃XNgyKCK(ύ%Y\\zC2µQ)[pC ]L!:?oY'<ہ0Ґ?@gmF&;xKC8[@WWuG`+||Rַm %wg@uTZa[떆VdĢO-HR1 ۲_K L)yndpp~>^F|~mvY^Ai۳4p댗.eI~_87=fFZ] !Ehoyywf]i2OWGh|PY[F'5䴷)ԖM;0 - lWwG`+XiNusd) 2ez$7#%=Vm9X-IihMëR]zٽ@rrK9ḢL-OQ،Cw K|?~0Ӷ<h{曕`ceQٜdFUmMWJVܱC]UrA.PIVG(d"]-ydr0ڹwShɓZBF{[>MrK32,} eʯ_%-z`Ce–_YYSHUĶ:.~)ؠH{rJE9#&pJ,F[9Oʂ@eQG8SCa:լn$Uu%6t6bgyR:3ȧ9@Ybjuyb _gCK(mli;mky!c.6!?99)~a(pႜ;w.ֆ9zͨn(D'ay@f<ھxk)x$gqe@=ҞIYܩ#.RHy=Gpvl2Q9$:!<`[R:if_hP!$Kc:C4&mGL1AM'n{7ͮ^^* yDziŖe0X?|R\$)oXE[>kf ݉?,m_ yKc>v́Xs>T^ݙg9l<qVW̓O5Ԗż!oi̇Bl 7sG`+}ѡa4m[b386n09b'7#/(`t`0Px'ByQ*Sڜmu c[ >-Y?cm#>'bY]CPiţ[Kz_rpṆ?+cRH#3m|QzlڌG?KzQފGyK#$EQ]|wGp@0mU,cBY:Ѓ?/ΣnƧYd- aio'9mB'mG?6Jم –u@-q-Ͼ[f oua=+ی+CY⸬g[чm>I$2??/###O~Ei4>MeʥP6g+6ʙ7:9m f+Kd9B e֎˘'5ؿSG`gqp-"@g͓]B,샺y96X-#}Qf۲=b8z=P \8?!ge}v޽+]QcJrC8{B]փ,SPoƲ8ql;J8. ` ::V6$Yזo%߬fr;>yK-o&[Y\F2RZm]ŀӧG*|oU Uc*"ڷuNVXJ<,eK䣏> /`x񢭾k<V; ce1A&[d,s3,aե#xG C|7mڪY] ijUneݜ,`Mf7V3=2rQ󣕀ГSw[;دD]8~#坠 NB ,7˙wĘ*֥Y=;݂o-p=GB.cǎI>Bf̃.TeZ6R 0kڜe,g%&l<w»}Q{X'0]vjĂXtjN}5 ׅh9\SGp:@;R+ɐ2y-#Gvuwf`htɒ|$C%AY{ɝ;wB=I=\}v7ppG`[ zpGS;&<,x(?&=3رA^(nf6m>#a\sG8*xtT,tGEJKSe\ك7zdª|2)C tٰ466"6Knnn.L%zrG !A8@ WJi6itVLEUuCKUv;Y xgBϝ;'SSSa:'O<,y8Aw;"i:#)GA#ז+72\E\_YHdx6;?`U ]mvk Y~apG`@i@!G8,4  Y`iqBn}Hnl.kgޖի 0P"M ,Hi9#p`[\QGpIEv!}JFWV/ ',:"`w+n###}J#O|GĀ>MGpv$88+I{$;}R1X?R@7m\~Nv#JG>eGpu:g p\7{ZQ_e)!8b <aì>%MuI:8)}"#tԂOC+I|wu /.4%3=YbxPz;B7s8Z:#%调<#ҩqCAFnd}xMn '%|f 4> RA":pGh!Ѳp1X"e:*oI,˅ϯ$JqdKSGp ~A8" iu"JI 4~x;Y%<t-,?ylP*4\G8"G>MGp/+aㆵݭ;(ed%T X]#ֱYE~ƍ]P^zI sշ"G fKCe#a贒 s2qU P-gƷtsJR|mJWZ433渶&k'*oہO#8"RHy=Gpp4,O: \ҭ4V<۞l^n~1$sʐ=Xv(188XW;PSTes-NG 6CG8WU[=G]I=쓅;zdL9ʐH)Vυz8q"(J Ǹ?G}oS[2#!ޥ2Gp p ,OgN,dAv6r^k )xʠ=6-Jݑsnf]vnH)-FxFٳӣow+jJ!S.8uJG>SGpB"E+sUyudJլL+?w8XaN:됳7fЃ),ֻgܜmg9uaqpmw4tG)tIQ||o &O/R98X~;QF |\CSSS!(23ٌ,.dde9#+/H$;UJpUG*2~\7AQ0'Ɩ΁y[yG8xtsG`K)ch)JU-ڤrEWg H}#OynwE3F,/dyO\{ kNPENȩ9{"žlrk(0dCʲV'ߪ9A-}&#l:xfN*+ 0VJpǘc$k:ꐑ6رɧQ+o]폲\`۔..du%*+7O:ŇKrJE_.JTc7iãs.sÅJ˞>Gp@4-X}el M*x''oI)OS~ۯ4wG.^]ȵ zHUK3%Mo=; @0G#poeO#8;F ύ":̵>:!HeA6۱[(ُn\wB4Z!g/ %py{%;:@"R5sG`O_ JǗt= c,|'|p|  eiV蓉ӎJ*>W~8K ϭɹ/Hm?boy[yG87pm!)摷uA:I+Ŭ,)d78yc\:ԁ}a<&V Gp:u/Md{>qD؂9e ?<4T;'>d޼/$j'<ڑ'NG`#k8sDGvF$Y\:/ 33Nc;vꜣ4z+^0߆G[38݋u/ޮ#8]d*Gxu^_eDڱc{ٲvxԎgWQO>$Y̮գa,/*Yl[yۯ#pog3p @)-<Ϡzmun_)ßzbLLo%cYDzHު˂,Ņ6<{+8Swʙw8 s;XvqmGW-Owk}P^cjG:z=z4@_g8(o;ǣՁ8omGUꑵ;ր5ъYW%ǵrʜ:@߀cޮ#8AΜ=XU7Zgd"- :$ J)`}}?/tJXaLi̗s)hvŭzq0& yv^౒ ^Dr%]Y BbJ3)r85vG`G\G(.tIFg3r/W2q}- b?H_}^).Weq,pUYl&T>L|^gP,jI4 m3*G?dʠY ԍ~hTln6啬&37臉}Z$o j{SX7ڪl_; :#еXDlG/RBdF72rU8c?% rrR6m'ƪUWʥr2V& 4fBK-#m}srQ%ѫ@M$ѩ` X 4Kvudq" (n 癩o ȗiI $L|Fx<8uEk8#8AKC[{% la3{I^ra9x^^\6/y~2?)Ji% z#K/%}sD!*  <[գ"g%nj?dt23+Ɵ8*W4T.;s@ |(1#۶e{~To룵q0i'ݹ#?~E^dQᬶ{SGZ :<$yy _}z_}L/7'ЗvRVofH}Y>APN^q9pNyM_]K:.h^^͉jX+Zud5O"RKׯT2 OXr ނgRp" \GpYZe4tbwλaٻd\EY[jKTR{?w0[ߙ%y{(Y&8en`vݸAӝ˒YoV:Sy)j{8 kVx)8aI:?)N[g6vNci*fmFJŬ|ۃ_^Wb{l3'G.xyA>|9y:vG.^N+$_CD-K9[n S/jE.mkS\o@xY'Ic*?[Vhq-+Y|C3<mQ:NG@8#Us޷4o>G+O]YR)fs]>?$WrL+0!7~w|\F&=+NȽkx )qnyt 78gvYμ7 q$b~}U5P?U(҉?^e;q9)uC}#+3S~s 1*>y;&1a D0Ygcy}G{玽GtGc N^Yr[N xK:ŲR_7[[􎹐B =!xAp]$âQapa %)~On%}מa lG#hk܎TMj5IlO&_˿3Wy`^{cd _뱝c[WW=PڢUЊ|6)S8@W +J]aWpD0T^:^pÊ!䫅ʜ>o$=b 4FЂ) ~N?l2S|GhHnsjUۄ现ߝÿn l@,JP&cpRC׍!GXgWB]S2}[ izZ)8#K}kE6uC=^$D:vS87hx4͹hAIؚy^.Ƶ>yY}V=oc}||cy=G;P~Y\?^;uڵ#+Jl%_;|q}UQW@Gsx9,'Gp,ts"U$vfI/o#PfǴS,J1VO=(Jv}|uG`gx3#8렓|Pu&U_}6Џei,-Y'Ұlz2=P=8m'Sp#8{@hcp+v=^l.9d剷 mDf0X@b\x}GɖŅ\}Ո3(,#&ֹDvG @xPI+IVXD[u!_Yimӯ!R(Z?fcrŹ$ oh`#.__FA}OVWǿRgl1[VUNޜsDv]p6:y%Y/ɝ;`u{-8:ϫݸ`f0 M$\8rʒe[T%y[UK\JNXlZ$Ƣb5"3ht~`aF7f^kϾ| gsN]ˁk>G $?b/MZ ;jWgr0_waM.=lVپƆU{@R!67[]NN)'1dgg62B'^>a]BT@H_}e>|o5ScxIp=xzV}18|J\=@[7<(=Ҝ{idAO~,ۢ(K!Fƴhcx1p=iQVlo}Fj>_d|F V.mp^i+WTR ;c?=DK=X"\^ڢ}#PVATnϗGڷ^__Q;sb%{F> w ώG޾o,Xvn[ wry'گƅpbOWÝ|Q%#5ȯGS P:TD@ڗ6HܘR0q{"l_Wn{nɆ&yYb\ YWrgʻɉ] bRx$.1~L^I wF_'B#z[ "?,gQB)a+\XXŹ.OjfKOR4DžfxcJvκ]YզcA4nōpN`Ɠ PIDATUuI-BYd]K1iѣ@PjHD@D ڤqz|{>t{Jv[7'[1;7{oFݳ/G_iKGzI%l󔫄cz Tzȗmy9<N\JD𽸍M?H{B }> vA -.%wJ]je$_IJmUjIVt:lʋ Y DD@چPy7<ct֧$a SOXqnw%g#X$ݦGazb٪w_Y}y/s b}ݵ{f6~ANM/"|%{𝡾𝤅Bv[Tp%Jνݥٙ}7j/^(GRu~H@PC7PlRQ1dE@ړR{F%" Fon޸GaqP$}1+/pO߶ѵ"oo?> tx.oWB1^P[]oo/-7'Nk_7__w*~\ o[ظrk+kإ+5ݥa{թ(ix_[G͖H>v7|هo sk,;PME\eE!h[]@;&oiAD8zA,ݵWƬR΃ ?,L:q2Ea_?h{)Bi΢߰3t0P応JGmgO&*8tS'lul|}χGvޅA$QnR:?is8@Bbsǔ8_.%8?gSvcCa<9_^߾Y4k{6 Ne=Y!^)g[~-|x|X,ۭ3fՍ; i;XWl|@ 1F-6~#_x{oOy%~p l1wvֹ0' D¸NS=A 7-¿цG/K?5bA4Mj/Slv>z͆O8-u)&7G7֪l=.ͤiKַPl?|{#kL+"zzjQD@:6r#;6yOX_cR%t; ?ywN6~S`X̙F>"+o|kMqcs3x9^qO)b7O)N2cz#scvgr=nҍX#TI>>W]i[B>I(9 I<su-"g&6x@dҎs1ݗi}') "p`$ %oؐf'n)X6x~?9i_ Xx䞝z|*a)_2ك+cd݌=u9}% 3*# @IDhJWl$ Rnx rz'gS$N*Z>ٮ5G)(k5Icg6Nƹ5pn*oz>wO׋޹ԩwD@ZO@BLբt$lA>I!σ %;KvKTO޶'"" $!nP ,;ͷqDAT HpB R><‡]|71 G߂MS)lF"t6ciF87S6F!hP;=y*NץNU1 #^u/" H9l cqxD7Nݯ_ea\q#N҄_I^y* ,a[FF7l %ޭY>~>T (-K5<J,ms' jOq¾NcLt<[;="zʋ cE@D`7yl?1(66= V[mN/tӊEQcD ֗gYI{v_iKlc[ФCѩ /*UAm Dm~Gwu8ډvvv\Gf7jO]Z/ߏ?qZ_&_D}H(Zh$" "yK}[4<6>q[yjWRџ֙~`>҉SEwv-Y|f _ڰ*i.?14'̛oPEھľGl},/f|>_g]>qʊ, E@D pc(}L~U&8sJvS7m;mSjf>nS+v[{z}KQQ"}փ%,9mP߯oFxvJUz-.;Q'LU%\g-of_\x|rj[D> |T*" ǒ6s_ #*vYhoRwÆ߱sA)E@b,Ȏ>-Y"Dn\f?nMX0G!ʷ0vbh8Z13R' J9ޙ'AR=6|ql+ė'l#V6Д >~1v9#c+tI`ud=^:> gE@Dc` $n1 lqW~ℏ r7t1VZ(N'z/}/i[fR_*n(0nf,}C$ϏB!gWPN}=:kUhJ%lK5!qbTŊOة'm^L!+/Y` -YB8Gx;#f$֩WycA$Eu Y}ڇ[Pbo/I_ҁ:e[45#""J-fD@D `A;KKS7T-~GCS©J뷍>é-?jӫ;b`WUosf $#BYsj1uV%Μݰs VͧڹМŽ|} ~VϪP:xAD@6q2!Hfu"A\XZqh٪,r6V V][%Z P: b+W/ڏ#?XpM*7\Pp|~9eLgl7s_|r(kubr K㨳SXw\ n^\/;V4YctȀ5L%J#ÆqŒYsQycF~77jf%q8h16}Kw:'+v5kNްHOh[Jm4/l (<@v>@~l~07~o~Z~^V>U }A<Ǹ/lݙh߱jhSݤŽ_1y1Y# ~k@Gf$kr&<𮒏F~e[¯8NocyFw<ϋ-ƃmOcy? /w&00n׮'4NׅPGID= H(hT" "1qq<A[ǃrӧ5+|l ei`?9b>dѧ6?;4"V>ߵ &\F:"  H(f pM7Zok8?2ᜬE[YyƳ,q&?H>>E 9{g+_ wtG?}7HQ-zX=޼T&"p? ("" "GRЇX{,bHC_9&ocFcG Ǽiч|P0e{Y{gbx]f.fS/MXo6K/{zt MB cȚ&l )T#qzܰ>t}Ŧ?:a}ן#wlaI%} Z FD@70כ>q^Vq.1g}}{X# c/c~|֖A0~ͮ<Ou֗eXlMӧŹi},EHBX(!PWj̖X[g>1DR*((8%p-z=kD}N@BiwF!" "bȳYoy^Vm6ޜ|>1vl`1/S8Q4.̛}oXZꕱ~\Mi:쫝-9G` >_*n)klGmx!-őnp <$& ˣ7Ȫیin#9:d-|eH?E8JYB ">P,(P//o -ltܓ/G쁏Lm O-׋-dž@PꜵHE@D@D`vKBIO^ łuf [gPc{ {)lEȋ!G\"oX{d!"$:b4H?% X41N;{[j=^P(n$3@B zc;^j6CDcH(uRi" " "?Jh>a!xbKԨ+f7;aCOU;}n.\K 7R#/{c6>0UļM2(J\D |R,ѧ@׃0Wɛ'!̣y=:sm#/dJLhǟۥϘ@gPu(E@D@De ^3O6KyQyeﷅnjiVm`hN ӫvjd%9PƢ yüo>Ɋt Y+TD@D@ZF… >i)xHn\mđl/8 -C8آ,+$ocyCgl76x.dyX$ĕD@Rwf!" " {"= x8A@\G<K{ x G'r~l_֋mq O" M@BO}b~M?O,pxQ #x.-&}M˲eD,yD@:R/& " " '? } u>|/|MBEی'?&>򾬖8Boe˘.Jݵ7*g72Zԡ_:}X{ ?+xln˼}D@^}k@K@0eAC|~z{ ĘG,82" C@B{R3!J3my Dz~"Ka˲>>c?3/+"${}5;h 6<-WϢ 祹X0_fe j^Jݻ@,^|^-ekԢeqqY' k@ bӢs>Yy|gѢZ~\/"p|H(LE@D@DA򾬖FA{:>߬>yɊOJs5kh)/|pk6vvo~T."p<w:k" " "p8 F:F|]_'+9=" Ǘ(ߵE@D@D <80_QaS^2)+"  ;JD@D@D@@,L"||Y7S7>Wy%]" " " mAf$A -h(5JJD@D@D@ρU" Mi@P%E@D@D@D@D@% ,1zJ]Ě@$%" " " " " ]OeqHP˧IENDB`robotools-fontParts-26e8b8c/documentation/source/_static/full_ltgray.png000066400000000000000000000553151477533125200270100ustar00rootroot00000000000000PNG  IHDRjN pHYs  ~ IDATxQI0bfHkX X ! `," .Xp@D4H]U<UWW~'rί;n`.;K)Оsw];\/U}Lc @a}PY]B=E(h!*~Xx׏f%t2ȑhWv@9K)_OjtؘlzkZ*~PK:si:J %'`xK\_aAaK@Ibzk&/^jB\Jmnv\r W9,H_<ఱn!T86$nl)_Iq͂nUJɛ.`Ab#aI`;^=?m#t)G!'Y:+ي{am (T.pW5pKs@nvj;^b X`JzƇ76~ 1^:;%,dw(OِD¾??f409#\R#@Yʹ(Z.]'o%a8"uſENPW;3)`~{w= ;hSuİ;m/kx@Hf@r;qp9EK)' li3@@@@@@@@w{љ/)K ӓsݙ5uJ麁'H4/0Ӝ2wWs>78t]9睈Q۞qS}企k!Dy8s`_ Q;x̞" #P{&H̀9.|4tumuXb ɿؤ8k:'39&r}X8P9ي{,^ŘF^<9:oMorR)~o XX1 ·sW6+H̉C,߿xp_ã o{tԡ17N4q띆7X)'xoMq#Xk?8T˲e"'@K&^eQk@< /58uk")i9LZѯmr$ggyNqt?+Ӳ`>`F`F`F`F`F`F`F`FW9n] ΄|2t`zr;.5=ǴKB\FNu)mg hoꗻoFL]׽sNRJGڗsލn8뺣җg"GK,瘇.5;Ssދ)²}S9t]o r՟y9v]_Mۈcl(r~#boK!t@r'k=d?,39'R}8"9|@~)vEJiO@;Ġ&aAi=!Zm"foh+w3wo7O?)} bA564rzOx@K.Ƌ} 7)%p^LS s_6$΅ g,FfX_AH(s7-9/bJ)kxfV?vD1&y\,ȑQbl@RJ^"r X}r{C9}zY~467(t$GlШConx?ߞOz%4&b} ſoHAOТM=99[=<Z#0[JP@"aG}T nФ"[F?)`~ &m=qJC zߴ_y=m9+Y`nb:a&缿SZh=ZiK|(((((((((뱯s~u~u{]hcu^3<,947"|uauݍԦ C9c8۾/|3rFP6p9OR:j".g.ŢE+ĘEM$b1qKk_wA/xEYttQ? \SIyy+k8yf`L}@Ν\LOD#>F6p)q՟jxz&̇41Poں2&6Q7^7x^b;1 6b"oXGNSJKh ^[M֕CL۫3ſl1A(?A6ٮRJM*KI~/%.l`"k0)>&Jz*Q wMONsJ8 /Nмi~_Y;a-QŏEXyr}jDR*$E_Wc6rα;ryѯJ9;|DC^lj{*Grh^}{+ulBs,i{3vPqW?mg`SoF:"v(3 I CSI)ͫ#jڿ"NʀEj5ooSE:{"u vډ؎y} ׿Ŏ:1&CZ*֫b( -%7oo@~80vkR뼼!ޟЬɾ?8U7g"9دߣqosdcWmY W(S^cb͕Xs db]O?h"n雁@yK0Dӭ-}_gڼ165*esטus%yy+Cj?Xד}{-/6b-m @]`iVzA9|DZȟK_pOE_P9|1(n:/oeHsq+zrT,4FiC^`LJnMNn.!}|7^eⓢ>%u1͕څ!ޟĭɾR:&T.HSfA Nu6s793 )*&r [S(bƇ{:_>p4'yCj?ɡYד}7{4HY/ϰx›#n}Hp?y 3v#Qc_> ߬#G(zW66k1M#6Į+[ЧȡY}0Nhx:CcT9d("0ڟPJ@w4VfCc# jzW4Q ǹ-%fm+s1Ay8JaM o?<w n1F:̤yi ڔzIdj0}?5%ci1ɑ1zb+6k_l&$ !'T,6+:ogCW1L" 𬾽b] ǐh- N6) '߿66E?1^4r5XNqtqmZysw{>S={~:og }_^Է{9GNHf.Z_d\+?#'^q%MrίWnԺ5k4?gi~mdqsލvi$Σ]eV4vM.~^{n6:o'.WY4V~Lϖ6PQQQQQQQQc_%|u~u4Zn;8t9맢NuG]u]StR:͎tהEI9 dӮV/-ݧ^o9zbۏޭfe^@9jbv_c6aUn~(wWr u9IĝM+Ğ~^ܷ">x`КO4Ar]9l槜s!.eH}_ I'6Fq'!|6FљiP ֻF;9#R:bDd>JH'Tx`l#s>>:3 C{vZ\2ȃϝ:r} K)6R[bNSJc9RQU>]QƩckW6_botp}c,ԯ}7WNJlx~xplD7u$k3t|o"x,Mur ֻq:SR '!5+~͋},MUg}Pנ.?3ī$Xn yJliNtx'9Ryb1;5a3y=,J65+_n{5KҪAY?jaފdZ`]G3 -o[L8v7[m`MW|9RUETRtIq|K*^hop}eeG*'=Vϛ!OE2=NѢ)OK{vߞ^K Zj5}1g{xOКw#=K'"w@r3/8C-U8gɾ4u ˦idփ I0w6ijPRC-7̑ue=HQЪT^S:ߒW:kujj{r.UjJAo)6M)631oe*kYR}ɾ5{U Xf7Q_~b]%c'k5\" Փ}{KA: ѲLeYRJKL^?Gm2qu1[]%$]=)!6U% Ŷ[g՚'('߭`:e~m Al)^$C!'ș!c iOVH)&]lp~)5kq8TRVPRT\2I.6h%WSLt"]a֥vLt{ >Łs_wm|Y;IN9(pwhk[;gRs>3Pя}0Nh WNa"@#AА(|bRJN5v эDQTN/Q]9WʞD1 Q0sUMbM_rI(AO2v{{Ozxo:do*6vm2O+\PRh},ЍqO:59R1c]1-/߹#gbD}۝!XZ(5sn~;1~KcZ.blH=9F`U"o)'9Dwbܼor6UMb?'=ߺ-朏w9mWCxQ*6y/b3Ga"iRu9fA(ݱ>!CxHlr55Y6283QA:&v&o:徳QU~DfC/?,Ӳ`>`F`F`F`F`F`F`F`FW9n7vI)Oz(0vw])˩7qĝGq=aXr5#wbkr2rBccrì];6e%^+s{1ߛysj}Bۿ}/":muփA9ĸviMu')5q\yjCr1I+7T4sؐ?#7NU9/4sSF;4E(XƟW STM06muAb3fSJhRHz0a:9RH9 pmYdFDR:nRs}.yu$3=XZe$_ =Ee s^f1w)VqHaWG9R;3-MQ@97SHZed?[Ub[N?qf3{eK>Nq87HUˑbU+M<73iL9.;>[i- )HNҀg(x0 s&ss3ŃDdRl*m@T^)WDy0Uc.ƲI;D*(S뻴}{+i7u(.fNZKҟ(lIs~zP3s IDATTG=p*!Rі))LьP"/E ~ط'G{52EiUMl68cZ5p ,C/GՐO=ͷY"T`Ƙ{]}0I2<Cfɾ]{y.%joSj'~i3`|al{o.-y?*gXP2vkT j2wiɓ}/kg,EckR. ~Zks笥˵z^2軴ɾ%R.>0/mg,TS1DJ8!UrE[s3'Pcȟ:u1+4U5YWFqҴCj9|*#;K6ȚRp 5#.cl/xZZ'>OyooEpɭhߑhkb$y:!5Hon#w̕7Ws>WK9ƧOHw͋\/-m`Gٷ.g h(8v2w ]Th-)h c-<1qۏ ,c9 n"v^T>OϞڌv䜏b&Q6JZFm3 6Cg*ogeco 9Ok ùzm6?I{}tBc%6e7cFo(ܞ+|rb><967Rh3O)ZQ=bNSJ3n"|-JlY7, iaZQ0NoϜv|3N({"c}ͣ_]EɑRP>(E9_;VPfQȞLq 5o"muO[M<%4$O; =6 Ww߆V^ofUgBfFiC㰊7z~*'[HeqXiG6Sh@2v{ļEiAUi3T>1/glV\MP k,loͶ߳q0ތІ~mFTE׀;^/=,f&)=i'SQ{ѷ'[mܰ$CN =$lCcpx?06fé݆6>>,6Hu/Jm8!?hnų'Svxq}oAhPai67a!IֻZc8G^X ũeڬ ,I]ӌ͸Rd6.9Zvox5V't!3^]%?L6yIݖ`<\b-6TWxLW:2JWmV,I]̡qR:yuOtU}ZsgfëF1ׁnáT~SEF9RWhÉ!wo)lxlTuu'f\_?W*.77~#XӡHOUIqS(wu!ø OIdi c(췟&ĦawuؓC8pzci F΁ e=cc&f۰i|6?.64JU\I'He}T>rpo0ڌ|{lPU {2xwJ󾜹qOཡxa#6>*Ї1a caݥ$&?O'57r;*~sᵮ/6g-@ly/qiMms]k_flJL?As>.e.M҅ޜs?Ij')˚qȑ18і[e򽘛Nbfn"Fkf9X0'sՃ.wx<{2'37b&1̈́c{1cӔDQ1Vl0};_w6?wi.~SqR:)I86FE7nKCR# #G: R"*tR\tX:ޓ㚹[5/uSz@)󈯓;(U1Z6g˫#:hQmMh2CR$K?i,mŸ.=:? &a}025J9WcnnFW(Cf^Zԧ17[،HhJ0:1QK)ZGX{6˹ؗٓԞ̬DL+su?<$-џ ګxE\D[K{N 6"'$W G61*b [2~Zd1F"Gb>54]Yf!СǺ;~ {2ݓ}0aߍŮNNNc\@WAqſѧviբfRJA+Y\11-ܩdslmȑcyX#o0UcJ):}W=UٓI$LУcGJuLZ-7⾓sމj{h_=sދ6irKXQan/s>f=&Ơ8eD;^kkn-9wqxWB&rSD,jew2і^8{2k}Ofn$4c 0=[ CfDfDfDfDfDfDfDf_?*9=Ma)/S9]6p)StRns}nR&'t>5wۙϘcpm_RJc^qFK!G1e]gA֮Mbbk-_1hrwwͿqu]׽bTsSYb"?}3eW'}Qu]m7p9SuuY?Vjmd6;n;Xn!GۈkbmgԨɑhMu'簮`b@֑D\\ٓD=%Ѐ'7~$:*cR:lG.eNA IwOl\6DovcL7~ >nGJxm2HLHIʓGu7ʸCYjA]D[*bO{2K\@#_ qR:h1r \]h"Ť(:KV4*Z4kԜɑ*?~ƺߨ:.]Ew1W֍]x.>Фch1͝vX/|i"IoGZQ@Տ9dٻn}Ks?q;kb*U\9иc~+Xg!G1)ލjWfYbLʫ1C| vo2)5Oj;~f,u^X.{OY2{2UԚDC˾8jmBv&:ڨ6֜x%^Ql-"|¼/p8cFS]r$f`9֙:2}pWsѠ6uEWOu Ҿc.vJqH]o:U]?`yں?Vru֥Ce*vtX17?uYs@~òܿRӠ[XוoxeC9_&ajPu9Џh}oXK@ZRQg=)[5ȚRtP"|]zu1X:X1bURr!KN1 /i.R}bbJn݊%!ofurR-qE9~τMq7Ɠ=Yԏ<׵"Qc1xmqA41^Mr$sqI2v,D32.?ou4w1WdQ}Of;1h-333333333򯧾Jyݮ4|뺳儮_w]׏c;+c亱k*mu#Պf0缳n; \ҔR:kᚍ5hg\i/ő#MēVⷝ_mRcgeYp,\Y6џJ1|17"nqu5r.;.Ko^-^ՍU|OTi`tf#w-[: kXbnRXlc)xXCߌ@iqܙ9ŏ7Kkhzcalp("TU q|EYh^UJ+F"G*N@KFm ≜:&Z F9)=1_Uoz]ea(3I؈[O6vճ=r~ eLq} ljP3&V []%9mYW5Kl2o,ѰƊ̑+fڥ.yLYb2W|ݭR3OMP /^joys=Wcnu})iڲm@25 \duy˲Z)R>{~Ζ~t5gúI)_SJ}uS-Y$ARWMo%>^Ke͍Cc| &rR2Eַ]nn՜:r91&Okles8_,WrR*ibEIHb.Eu˕n3Q=uab"nԜ_~?.~OK166ѷq;TVcÊMUc=yARJ'#c;Gb+fqcmwr?X̻/w? )c(Y;n`16?3S rsGrU#IH9HQiXϲ5*E[c'ǁ>߈ᢩf&mbi"_=ϱR2_ZXLDk 굍ֆ 2lVrR:^L_^,]n8{1v2>وZ()4E%jmfo^X}vx=7ckRJ.F:_c[ =|D<3"a|ؠ>Qtߌ<GKFF|/ro0Gwo/rcM܈PV&pN?WWo6JٗS01ZߞCuu0p'3N-Wyge ^xD?FNZ~Sv5ن9{WV!k8 m,MC%Gum99ﮌ-9WʹY]&vkרo,/bg^)%7~8-Ӵ`>`F`F`F`F`F`F`F`F`FW9u]wu]m bW]םu]wRصSXyge|ui2!BsR:ks}0u,E{ sߎuQ)LUk9Rr$% 'kw.k|Xlǿ3J,NV1 <5(>p{u/:IӔ /s'd**nCʹl C^J|mn*JYSc방{[?tzsLڝxzalB-h>Gr@Mr|#~/gXۅO69_|aj5jLӨ#59[?)e#?sDZDv`ul71J{rjH*ĺk-풯c5vۡ3ƴHMo}PU,%{v l=%F^VWKPJg]7e DN˕,yUY3«Ym 3SAhof'twjd-j);oZ WU_WCST߫h~ RJC#5=D_<,40j/fs _%eM ~e]mӄ!Mȑ'{egM#I5! dg(P\h*vgNZV7)RicyNSJ͜HL)ye(ȑO'QQbgd(bvU֮vX3PcL|VPw#@XcA83q Hp=X^H\s&{GiAZTޭIk֔}*q(8,R?Z|#%G"+9xiX)a`--fnHprm\;[/l"1}Rh(LLZbcdW6Hha8o'n#_GdnmG9hZ^&NUDJ.ǎyC/ю֮ yhcZd>YgK|((((((((G_%uk b)]3 9t]M(2en79ܯ[ \䤔[fsھ.'z?skY$bh9HTg`$w֮2g0sMƜ?Ql畖YUu)^?#9u]wuݶO%r]cM3U]םq̀\8Nb|O~"}^}w%G27m&9X3h뙱0Xf}+ 0}s .itJD 4zf,1ٜ1я݊mܚi oɉ2E\cSq{SJhw`ṪZE@saQ1'W.qwHEMvnM Vu3\snbUa0X~kXyOV7x[Zfs{ͅpJ'>el`=7)%%lX+ EA<ǪM )Ϡ1VN5M >A/:.*lj:&scs̉]Tﻱ9 Gh _rb͸rY:/p)+ǜX ظn?MNcґd|ʫL:?5p)sv1BQ*&QQ1 W1G7IQr$rNҸZ&3/]>\qܚ!&y_ qO,t۟wcs{6iD d "XE`6K"0D`Q2 :\B {/}/RE3}YԂ{K3]aݾ u>/ŷM,mэrlDS%8GݴHWba6Sy/7 *MmwS;9ivHΘAgZ_i=Y9ԵJ5rN9 [0xR:Wj/){oƏɾ_C){`'[/; g5eYL\RڋSG}l59ҡشf~3hDx/̢9E8>rѱ~{V;ylw7J)M'^֓]I Clf+L2HEc%7|VO >9"(xgz:tyDvr+ @ @ @ @ @ @ @ @{fC p1J) pVk9^) <}N;kYLcׁ,9Ŵje@SAT}m_s8j"רѷ8?;.)IU9ǯh(p߮mmƂ|_ 4T;szK y$jBjpmhCN-9s1yu9/;?lL_^)Sm_hd8 gy^c~PV/3eR޾ꏸnԒie){Pyy*?Re0S69{YʛҴ9JN+zWRb^h_@8'^v\\q|yWdGb HR/D)`NGްwDzOM q0ڛ}#zu9^-tOǮMq욥 <$yD. fSl-E`,:$Ll sFB]Ӳc~tR;-<3x,_j`kVܻ!iR{'fN 0I^5?RJ~Z 㶎NJ)`Yᙧ;#/\uU>6 .t-ςR,:r ͩc>k7ܭ<;j=50 9bvC90rĚwtzJ50CcFБe<}@+|)8 !|z;|/wq~)=cw?N`$N471}ǮɋZmc ܷtXګc'.l`Dnc = u9 = Cv;v,dN ;/5 z< iX``re~ ٷSo`lʫYJ)'{XqnH)z;z^:j=wPx:K9BǺ^ K/|zk*1#֕q~ߣ:N@ϝ /@:61>Aȋ:K)7c /oG@W.b;kvmtꮼ^}tʃ3% M(b(U^'iCyCݶ׫ZPJc|}G"GyTKJ)yh\>9s\Ai\ KI;/_/So RS ;q𸨬Z@ (_ 3G_,h}j5/ԕ;T5x\T R4fpSB) oM)y-B4(nKb?0Uǔa(S.V'%DQu%sMAS.^Fc F +r|Jk/So 5]ܾk)2 hC cgZ{RJ9pcP۷GE"С@8PlnKܛ˫g:  O==x҇&^*x,+grz⮄Ym)x̿5@+Nc]7c /L=581Ry@uo+#c_TkBG z(tΗ܉Ww40 [  Ny%*c_`DaJ6AX>O ܧ6N j~>R4^̓G"06$<#c;34t"]>SJ)|; >SO8 )W_-as)e^? C2G)IFC2/Yj\NJcyZ-Ƹ+tn|mPe} ê;ݏ'vYv9hq)ݏk"ǹvyБ|7͉R:,|s]j^{q!!$rY.æ]eA;R+φ8_5x\R-l~l"ƫㅞd\[`Jr0XLJZ,CT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT&41B}@RJG1cW27+Aa|}́݅.BgK @J8'421^iZ|UǔR. BTf13&dԓ&oyr:+uAa:|bFc Fä88zC8ԁ*!X qBT YSҁ&KY|gBG`93xkjX:5ǩrg<,EJvJDz:E}N)d`:^ ̝9&￴ 4ȱ;+Z#G{af4KGXMStڱ^a|P&@>??5 N*-b-58ІGc.ڠ'Z=ƉvvZW2;vrq)S&|hEQɘ0.,$kh1!x,ۮ?ERJ 0rNJmahӇj E N0qܺUg Gc3vZ@" ;Z`Ԓ5Ѕq~" (W\K`IwJ ta<]:aLeh>Ѕ!x,huhVLWKzkU,m:ab ,6(xl>Ц ^pg`Ll':ciey;@ NۮRdZ^sK=ІGQ@ 0nGxn {11; @Uq#T:aסR[ly<ShF]b6@Y:z;8vt|Rw`;% Cqם0 Ba)5oX۲_)B~{Go+܇ot+C2ÕR D3} +8c)e[b'!]Q8rBa5>oyXuҎEec.?^RJ'۝u߸ݏy7y:Mv8Rʗ񋾀9/慮`]Hr$]VG6$廹g7ȃBgxK;w\i1߇ɨ>߯<.J)} !얢q|ÔWׅUmP:r_C'rwSj@W|?yobHLJZ,CT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT IDAT4Bu0X?bW5> U.n&x}oR:!0@{U9ХE1 Ps-yPOFn^KΚҌ~0y~ǥ@P',y0:RJ!C 0yQ{췲|&| @' D2rczC8uW'~)\äm<_+xL)},p2~:+r\Dwer{AUN4~ЙՖ ̖&j< rxYt%h n@b7zF[+N? 0JY^j`IybWؼ>˞l~oR ! >;x^J)? o.}` eR;7޵xE~t[kʑ_K (/`O۷0?grNv<.J)2p?0yyx(p`Y)yGS`cc?~>F39^wv8'IwF vW{RJC}86o<Ε tNR \ @MekFa2kJ;|&|Z(]^.-_mцA;s1 }!l TB-kG$ ]#/VB]}||^XNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#Pm4iJi7Q1^6RuNA12߇je}oXЅB.BgHR:X%46zn}ۅ|JXVCG<0ljH)zI8/Lw[Jiχ0 Ue:V ¤q@ :RJ4i-gDz1nC/2 96qG"G13 ҭ2ρ㿦#rݶ|Y^2>_H[)AxŇʄRJWBG #eW4-2ޑdžXw:<8SOg^<*-h#Ljʮc  5x[? ' =60nHt3Ѐo1Ww2߿hma|GBG!޻a+Z/M#uv,t1ІGc^Q:rhE|h×CH0>v-Wc|3c\  D@,vHynGMZ{i h౬yhwI-6 KH)-Tƞ;@aLڶS*m@jyP`g'ߧ.< wuЁ=0 jI+Ɵi# G+@'F0 &@W?o(G=ta2:f >w]1M0 t&?(eLlxfK @u9xѬlBďҕ`Mo1Z:c @b?4<]v-g~Y33ZhagXm"ІS"(m93Zt8z()ƩxiX>дR,A' E1hН9zzZpt/ؔEaO嗭v5*Cf֔Rʏh?OZڂ 4P_1/b@eB*r|1x, @M_cWZ`b(*5G䎫5vk;`]7 g`rn6av5|{kRJEWkʡiJ)~~5qəfzwǹ N#ĕ`K 36|1=9v w5cKJm۲wZ/J)3Ge7ܕe#)!w\ӱ})|/|xCwaR.`YOO\A׬ 6FJi{S֚<Ε1~ɻ-E],Grjvc}Vhq΀v[&3GaXG]s,ʁI¯@äU`q8Wv@οq;/aZ+TyXv4my;a0߇ɸ+Y~qQyf^xA/݌yj4M+ D)Ob7 qaT||7PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#Pm5iJi; /x|ZscRsr_>Bkr!*~6߇N]YGJ6U~2.OC3!d-B;SoDBn<|}0v!LO> V^9)ckcJ)> 6w4dRJG@O +)ݲukjc<Ң0߇Q;ZY)xL)V=hB*?0RweWyZ,+Li&Zo*6Sv-h&K|k  BtOcsaZ  @/虣ҶNY[>bi?;Ќ&u.7v;S<06%xQc>x$tǾԁ|(`u?w<@%4qqR6ϰ!-MOGsi+`e۵=Ү%ԾCwр 9n@TcXEՌp\WU~ԥ閤%u^%  /h>Vv;Uܭ4xn`렗['φ@/NVn69筈(Ҧ>` jC:9睈ϐLvYEZ#NԪOC|v`VR+r֫] R]o G`ZZJ8_qIhaPګM&"#9g>VaS@}:sRRsw<ӱ@sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @ssޏًu UD\GeD\.t˼fMA9烈6Wй8S!s·q۫\X7%K)ꒇ< #WȣbѫǜV)%~8/qRi 1pG3 c("%t'_^I{Qcιl8W8K)M6eţc&n:|'Z"⟝?SJSGǜND\H(]凜ҜOm>:@35scso z?jsފkՎeNJv{H+Y3Q:qR{S/V<`6pb8OXco[s;5x7Kf=?LͨЏQgrq8[gII)ݨ41NXW9ў@s%x֬Ћ˱6lVލηZ05Wcq%F̓NjfyS3L[Rv#tfrNcow䜷"z"I)ݎ[x_;{q'"xM18dSv21W<zޞ Ƨd7~ݸ_њT xED(RR>j(|x0v?<$=$Cxj=N ocqζkxIŠojC}Zd&:k*reu~^Dn~ʋ+RJYٞ^](~`J>RR\y+"ʾ#[GRُk<.9m'`JXP#U7L15Z:: iy"x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'xmМNDG^lEĮ_tY?)k 6LfY90""b{zM DyD!e1\*#҅F_oG)K^<朷jǴ 5Z IDATV@౞x GUwDx1:Fv9^^< ñkK>  qM:0"nO!U=QX_#c]5C[5>c%ط9筈I}s&%Nwg^(`lr }J)]|.q}qQy/"i1TSJza-SpyuK^Gx7͚|ٜsDaD h]EiDu3\u[(ݕydKbq[:x\ ~[') ][\XSBȓ:`< /~0y_#^CTHzSs.Q|./&{F՗@v=胱`@_,dy pn#g59gg> 0P$I$,I#@s%xլ,ZpFJR= 0|ro4@RJ^VXByÕVsI/56}0@=G56u#xӁ8`} *:tR0tk5xmKq9ةXo㉾:d0")[J@֝{c=:P&c} t"6l{DU@P]>ӷ]SX+LUR0nU;p1 `p`U,xժOX/AX g}!`RJJyW*dܖk:Y8h0Ցj+R+܍@+b 0 rqN mO`V<Ӵ@sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sIs{Q>;[6".#5t By!xRoE9$Q8Տ.3s++|+]l#@m-:oWC0#FqQ`x۫/T>Lh@#@=YXo:-ԅ &(t[/`` n:-6騷WHhCXk SuU m+P@k`Xk*|0۩3X*Ot B1"9# 0 ?9C[ۮc 6`Վ0 +M.kFV;׷T= NuUda 芹$,V<.sF$缣жK bQz__R5xQ@1#'cx|ї@nRJ*ط^0GkZ`4@׌;knc @h 0@N8z[ O!cŤ3} tGX M=qQ}t"6l{ctX0BuQB6jeB<ɢ Uj0jX# 0 +nVD`RJY+R]f0:Y XO2@cwl6{se)7iLi91ߺh屭se%J } 0w]4Y0ǯ:xr>I=1>K|: /Zߗs>{ #ؙ<S^j eq tqc0+1$GX7s9R^&~0YeU$t'kw\30mH#R\ T@¤sy]VXF \)75&LY]8TqN NYa "s}j`tNkkQ#¤X%$x9xQX+תeʿCk<^)79:IT̕l~.`G X`ī:I< xƗ@~6#1S~xRP=AR:P౞s,Gj>{Y@v=胱`|` >j}Q0G x@#ЫҥXG=. "cxAtƱ=[R:;}t\1El5x"Х/)%$еS-G6%c_)БctO#`m{B7c Y:RGE&fq X5 @ܯx,tB`B.VElx<.#>VC&,dj;2n6=9ן@#WRՠsޏjU; c[TP0O]4r&t'+J^DI㦾?&&\XB9q_ sU>K]0])CKg>oWƌx/_sxD7XM T9@0l/ j52g{5NJ5|]x]=P ߳<%\(m\}AxNy+"# ,Z`<'Gkqb+ sޯ{ vSG̛ǹP$LJ XV \ƄIZv:i<!$O J "\Q*ayOR 0rMEuW/)v6^Х^O:\6x;WJ 0=+ @kG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sGV٤9罈؉=]u) ]@r[uY>['SJ׺`Zff_8\Bƃy5"JyRԽs%#b[èԹ|>y{ƭIs>{ɸҩ.`u%˜rSc$ܕ'-hRc FäIqJd %?j:H 0>o ٍ*e̡ xqrT} ڶ`ī:IFd#*xn.`Q=D#:`^p`ZꙎv)# 0,U<:he.d0@+&<<ÿ?Kݺ;gN4vZ` u =L±3 lE ⱊ v0bu} {[1o5bej$`,d G[.kF.b׷m[c=wDv}9`\@W4[x4p]2H>:B<:҇ 8K]Se uˣn 0Gk57xd`<>Kc@0Yz]`̓GEZ`%xXc@/T/*FX/n>#lh$x#МhNn m. KxܧQP_uЃ0 ^\ky5M>{F@:#kZ`4vR ƾ)%/@,z$5[\LgRr&xwbxsF.&n t=#xaB( X1{htD`BV."N42{7~zȜslW+Rw4.8#_X?SJG_X8`1#Vc`E xX/úC- 0U"IDAT:x3#f/9筈(!{}<^$s/|eajSO3tqEse)T*TSƄO)=#O;i  ,``<^UV?!|{+[aFyV?nk8:tLȼ9x9DQD2rW^8aK:E0rKs~0Z_jxoZZX.s] tS璧)K] 0n͂r{-%(eJBЅWCs]?u>i TDcn@ϳIENDB`robotools-fontParts-26e8b8c/documentation/source/_themes/000077500000000000000000000000001477533125200237435ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/000077500000000000000000000000001477533125200267065ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/intro.html000066400000000000000000000011631477533125200307300ustar00rootroot00000000000000

robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/layout.html000066400000000000000000000076341477533125200311230ustar00rootroot00000000000000{%- extends "basic/layout.html" %} {%- block extrahead %} {% endblock %} {%- block header %} {% endblock %} {% block relbar1 %}{% endblock %} {% block sidebar1 %}{% endblock %} {% block document %} {%- if pagename == "index" %} {%- include "intro.html" %} {%- endif %}
{% block body %} {% endblock %}
{% block relbar2 %}{% endblock %} {% block sidebar2 %}{% endblock %}
{% endblock %} {%- block footer %} {%- endblock %} {% set script_files = script_files + ['_static/js/fontparts_theme.js'] %}robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/navbar.html000066400000000000000000000104221477533125200310440ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/relations.html000066400000000000000000000006541477533125200316010ustar00rootroot00000000000000{%- if prev %}

{{ _('Previous') }}

{{ prev.title }}

{%- endif %} {%- if next %}

{{ _('Next') }}

{{ next.title }}

{%- endif %} robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/search.html000066400000000000000000000024401477533125200310410ustar00rootroot00000000000000{% extends "layout.html" %} {% set title = _('Search') %} {% set script_files = script_files + ['_static/searchtools.js'] %} {% block extrahead %} {{ super() }} {% endblock %} {% block body %}

{{ _('Search') }}

{% trans %}Please activate JavaScript to enable the search functionality.{% endtrans %}

{% trans %}The search function will automatically search for all keywords in the query. Pages containing fewer keywords than searched won't appear in the result list.{% endtrans %}

{% if search_performed %}

{{ _('Search Results') }}

{% if not search_results %}

{{ _('Your search did not match any results.') }}

{% endif %} {% endif %}
{% if search_results %}
    {% for href, caption, context in search_results %}
  • {{ caption }}
    {{ context|e }}
  • {% endfor %}
{% endif %}
{% endblock %}robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/searchbox.html000066400000000000000000000007241477533125200315550ustar00rootroot00000000000000 robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/000077500000000000000000000000001477533125200301755ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/diagrams/000077500000000000000000000000001477533125200317645ustar00rootroot00000000000000fp-object-tree.svg000066400000000000000000000303501477533125200352350ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/diagrams info layer features point image component lib contour guideline kerning lib font segment anchor bPoint glyph object-tree.svg000066400000000000000000000342341477533125200346370ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/diagrams info layer features point image component lib contour guideline kerning lib font segment anchor bPoint glyph robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts.css000066400000000000000000000454471477533125200327450ustar00rootroot00000000000000@charset "UTF-8"; @import url("https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:400,400i,700"); /* ---------- resets etc ---------- */ html { box-sizing: border-box; } *, *:before, *:after { box-sizing: inherit; } ::selection { background: rgba(22, 255, 0, 0.35); } body { margin: 0; font-family: "Source Sans Pro", Lucida Grande, Geneva, Arial, Verdana, sans-serif; font-size: 1.15em; line-height: 1.6em; position: relative; min-height: 100vh; } pre, code, tt { font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; font-size: 0.95em; font-style: normal; font-weight: normal; white-space: pre-wrap; } @media (max-width: 600px) { pre, code, tt, .method dt { font-size: 1rem; } } pre { background: whitesmoke; border: #dddddd solid 1px; } dt code, p code { background: whitesmoke; border: none; border-radius: 0.25em; padding: 0.125em; margin-left: -0.125em; margin-right: -0.125em; } dt, dt code, dt span, dt em { font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; font-weight: bold; font-style: normal; } dt a, dt a span { font-family: "Source Sans Pro", Lucida Grande, Geneva, Arial, Verdana, sans-serif; font-weight: normal; } table span.pre { background: none; border: none; } a, a:active { text-decoration: none; font-style: normal; color: #0062FF; transition: 0.25s; } a:hover, a:active:hover { color: #66DD00; } a.underline, a:active.underline { border-bottom: 1px solid white; } a.underline:hover, a:active.underline:hover { border-bottom: 1px solid #66DD00; } a:focus, a:active:focus { outline: 2px solid #66DD00; outline-offset: 4px; } button, a.button { border: none; display: inline-block; font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; font-size: 0.875em; background: #0062FF; border-radius: 12px; padding: 8px 16px; color: white; transition: 0.25s; } button:hover, a.button:hover { background: #66DD00; color: white; cursor: pointer; } button:focus, a.button:focus { outline: 2px solid #66DD00; outline-offset: 4px; } hr { color: none; background-color: none; border: none; border-top: 3px solid black; height: 1px; margin: 2em 0; } dl { margin: 0; padding-bottom: 1rem; } dl dt { padding-top: 3rem; } .rst-badge dl, .rst-badge dt { padding-top: 0; margin: 0; font-family: "Source Sans Pro", Lucida Grande, Geneva, Arial, Verdana, sans-serif; } @media screen and (max-width: 800px) { dd { margin-left: 0; padding-left: 0; } } h1, h2, h3 { font-family: "Source Sans Pro", Lucida Grande, Geneva, Arial, Verdana, sans-serif; color: black; line-height: 1.2em; padding-top: 3rem; margin-top: 0; margin-bottom: 1rem; } h1 { font-size: 2.25em; } h2 { font-size: 1.75em; } ol { margin-left: -1em; max-width: 100%; } ul { list-style: none; } code { padding: 0 0.05em; white-space: pre-line; } pre { padding: 1em; white-space: pre-wrap; } img { width: 100%; } table { border-collapse: collapse; border: none; overflow-x: auto; display: block; max-width: 90vw; } tr { border: none; } tr th, tr td { padding: 0.5em; border: solid 1px #DDD; } tr th { background-color: #DDD; } a.headerlink, a.headerlink:visited { visibility: hidden; } .note { padding: 1rem; border-radius: 0.5rem; background-color: rgba(0, 98, 255, 0.1); border: rgba(0, 98, 255, 0.5) solid 1px; } @media (min-width: 800px) { .note { margin-left: -1rem; } } .note p { margin: 0; } .note .admonition-title { color: #0062FF; margin-bottom: 0; } .highlight { background: transparent !important; max-width: 100vw; } .highlight pre { border-radius: 0.5rem; overflow: auto; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink { visibility: visible; } #header, #footer { height: 3rem; position: fixed; z-index: 2; width: 100%; background: black; font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; font-size: 0.75em; color: white; display: flex; align-items: center; } #header a, #footer a { color: white; } #header a:hover, #footer a:hover { color: #66DD00; font-weight: bold; } .navbar { display: flex; width: 100%; padding-left: 1rem; padding-right: 1rem; } .navbar ul { padding-left: 0; white-space: nowrap; } .navbar ul, .navbar li, .navbar #searchbox { display: inline-block; zoom: 1; } .navbar nav { display: flex; align-items: center; justify-content: space-between; width: 100%; } .navbar nav div { display: flex; justify-content: space-between; align-items: center; } .navbar nav a:not(:last-child) { margin-right: 1rem; } .navbar nav svg { width: 32px; } .navbar a svg path, .navbar a svg line { transition: 0.25s ease; } .navbar a svg:hover path { fill: #66DD00 !important; } .navbar a svg:hover line { stroke: #66DD00 !important; } .navbar button { background: none; padding: 0; transition: 0.25s ease; outline: none; } .navbar button svg { width: 32px; } .navbar button svg line { transition: 0.25s ease; } .navbar button:hover svg line, .navbar button:focus svg line { stroke: #66DD00 !important; } .navbar button:hover svg line#line1, .navbar button:focus svg line#line1 { transform: rotate(-45deg) scale(0.8, 1); } .navbar button:hover svg line#line2, .navbar button:focus svg line#line2 { transform-origin: 100% 50%; transform: scale(0.8, 1); } .navbar button:hover svg line#line3, .navbar button:focus svg line#line3 { transform: rotate(45deg) scale(0.8, 1); } .navbar button svg.open line#line1 { transform: rotate(45deg) scale(0.8, 1); } .navbar button svg.open line#line2 { transform-origin: 0% 50%; transform: scale(0.8, 1); } .navbar button svg.open line#line3 { transform: rotate(-45deg) scale(0.8, 1); } .inline-items { margin-top: 0; } .inline-items li { border-left: 3px solid black; margin-left: 0; padding-left: 10px; padding-right: 10px; } .inline-items li:first-child { border: none; margin-left: 0; padding-left: 0; padding-right: 10px; } .inline-items li:last-child { padding-right: 0; } .inline-items input { border: none; border-radius: 0.25rem; height: 1.5rem; padding: 0.25rem; } #searchbox { width: 60vw; } #searchbox form { font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; display: flex; height: 1.75rem; padding-left: 1.75rem; margin-right: 1rem; justify-content: flex-end; } #searchbox img { display: block; width: 16px; height: 16px; position: relative; top: 6px; left: 24px; } #searchbox input[type=text] { border-radius: 0.25rem; font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; font-size: 1em; background: transparent; border: #444 1px solid; color: white; transition: 0.25s ease; height: 100%; padding-left: 1.75rem; width: 60%; min-width: 100px; max-width: 200px; } #searchbox input[type=text]:hover { border: #66DD00 1px solid; } #searchbox input[type=text]:focus { outline: none; background: #444; border: #444 1px solid; width: 100%; max-width: 400px; } #searchbox input[type=text]:focus + button { background: #0062FF; border: #0062FF 1px solid; } #searchbox p { display: none; } .highlighted { background: rgba(22, 255, 0, 0.35); border: #66DD00 1px solid; border-radius: 0.25rem; font-style: normal; } #content #search-results ul { padding-left: 0; } #content #search-results ul li { margin-bottom: 2rem; } #content #search-results ul li:before { content: none; } #content #search-results a { font-family: "Source Sans Pro", Lucida Grande, Geneva, Arial, Verdana, sans-serif; font-size: 1.5rem; } #sidebar { position: -webkit-sticky; position: sticky; top: 3rem; max-height: calc(100vh - 3rem); max-width: 18.75rem; z-index: 1; padding: 0 0 0 1rem; font-size: 1rem; font-family: "Source Sans Pro", Lucida Grande, Geneva, Arial, Verdana, sans-serif; background: white; display: flex; } #sidebar ul { margin: 0; margin-bottom: 1rem; } #sidebar li { padding: 0; } #sidebar h3 { margin-bottom: 0.5em; font-size: 1rem; font-weight: bold; } #sidebar .caption { margin: 0; text-transform: uppercase; font-size: 80%; letter-spacing: 0.1em; } @media (max-width: 1000px) { #sidebar { z-index: 2; } #sidebar.mobile-slideout { position: fixed; width: 18.75rem; max-width: 100vh; height: calc(100vh - 3rem); left: 100vw; top: 3rem; transition: 0.5s ease; border-right: none; padding: 0; overflow-y: auto; opacity: 0; } #sidebar.mobile-slideout .sidebar-inner-wrapper { padding: 0 1rem 2rem 1rem; transform: none; overflow-y: show; } #sidebar.expanded { opacity: 1; transform: translateX(-100%); transition: 0.5s ease; } } .sidebar-inner-wrapper { width: 100%; padding: 3rem 1rem 4rem 1rem; overflow-y: auto; transform: translateX(-1rem); border-right: solid #dddddd 1px; } @media screen and (max-width: 1000px) { .sidebar-inner-wrapper { max-height: none; padding-left: 2em; } } .sidebar-inner-wrapper a { display: block; margin-left: -0.5em; padding: 0.25em 0.5em; border-radius: 0.5em; background: white; border: white solid 0.125em; transition: background 1s ease-out; line-height: 1; color: black; display: grid; grid-template-columns: auto 1fr; align-items: center; } .sidebar-inner-wrapper a span { padding-left: 0.5em; } .sidebar-inner-wrapper a:focus { outline: 0; border: #66DD00 solid 0.125em; } .sidebar-inner-wrapper a:hover { color: #0062FF; background: whitesmoke; transition: background 0.125s ease; } .sidebar-inner-wrapper a.current { background: #0062FF; color: white; } .sidebar-inner-wrapper > ul { padding: 0; } .sidebar-inner-wrapper > ul ul { padding-left: 1em; } .sidebar-inner-wrapper > ul > li > a { color: black; font-weight: bold; } #mobile-nav-overlay { position: fixed; width: 100vw; height: 100vh; background: rgba(8, 27, 60, 0.75); opacity: 1; pointer-events: all; transition: 0.25s ease; cursor: e-resize; z-index: 2; top: 3rem; } #mobile-nav-overlay.hidden { opacity: 0; pointer-events: none; } #content { vertical-align: top; margin-bottom: 1rem; display: inline-block; padding: 0 1rem; } @media screen and (max-width: 800px) { #content { width: 100vw; order: -1; } } #content h1 + .section h2:first-of-type { padding-top: 0; } #content p { max-width: 70ch; } #content ul li { margin-bottom: 1em; max-width: 70ch; word-wrap: break-word; } #content dl ul li, #content ul.simple li { margin-bottom: 0; } #content ul li:before { content: "–"; position: absolute; transform: translateX(-1rem); } #content .toctree-wrapper ul li { margin-top: 0em; margin-bottom: 0em; } dt code, dt span { display: inline-block; } @media screen and (max-width: 800px) { dt code, dt span { word-break: break-word; } } @media screen and (min-width: 1200px) { #fp-object-tree { transform: scale(1.05); margin-left: -2.5%; margin-right: -2.5%; } } #pagination { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 1rem; margin-top: 4rem; margin-bottom: 6rem; } @media screen and (min-width: 1200px) { #pagination { margin-left: -1rem; margin-right: -1rem; } } @media screen and (max-width: 800px) { #pagination { word-break: break-word; grid-template-columns: 1fr; margin-left: 1rem; margin-right: 1rem; } } #pagination a { display: block; background: whitesmoke; border: #66DD00 solid 0.125em; border-color: whitesmoke; border-radius: 1em; padding: 1rem; padding-bottom: 2rem; transition: 0.25s ease; } #pagination a:focus { outline: none; border: #66DD00 solid 0.125em; } #pagination a p:after { font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; display: block; color: rgba(0, 0, 0, 0); transform: translateY(0.4em); } #pagination a#prev-link p:last-child:after { content: "<<<<"; } #pagination a#next-link p:last-child:after { content: ">>>>"; } #pagination a:hover { border-color: #0062FF; } #pagination a:hover, #pagination a:focus { background: #0062FF; color: #fff; } #pagination a:hover p:first-child, #pagination a:focus p:first-child { color: white; opacity: 0.5; } #pagination a:hover p:after, #pagination a:focus p:after { color: rgba(0, 0, 0, 0.2); transition: 0.25s ease; } #pagination a:hover#prev-link p:last-child:after, #pagination a:focus#prev-link p:last-child:after { animation: pulse-left 1s linear infinite; } #pagination a:hover#next-link p:last-child:after, #pagination a:focus#next-link p:last-child:after { animation: pulse-right 1s linear infinite; } #pagination p { margin: 0; font-family: "Source Sans Pro", Lucida Grande, Geneva, Arial, Verdana, sans-serif; } #pagination p:first-child { color: #081b3c; text-transform: uppercase; transition: 0.25s ease; } #pagination p:last-child { font-size: 1.5em; font-weight: bold; } #pagination > #next-link { text-align: right; } @keyframes pulse-right { 0% { content: ">>>>> "; } 15% { content: ">>>> >"; } 30% { content: ">>> >>"; } 45% { content: ">> >>>"; } 60% { content: "> >>>>"; } 75% { content: " >>>>>"; } 100% { content: " >>>>>"; } } @keyframes pulse-left { 0% { content: " <<<<<"; } 15% { content: "< <<<<"; } 30% { content: "<< <<<"; } 45% { content: "<<< <<"; } 60% { content: "<<<< <"; } 75% { content: "<<<<< "; } 100% { content: "<<<<< "; } } .central-column-wrapper, .central-column-wrapper-wide { width: 100%; max-width: 50rem; margin-left: auto; margin-right: auto; } .central-column-wrapper-wide { max-width: 100rem; } .columns-2 { display: grid; grid-template-columns: auto auto; grid-gap: 1rem; } @media (max-width: 800px) { .columns-2 { grid-template-columns: auto; } } @media (max-width: 1000px) { .hidden--small-screen { display: none !important; } } @media (min-width: 1001px) { .hidden--non-small-screen { display: none !important; } } #wrapper { min-height: calc(100vh - 3rem); display: grid; grid-template-columns: 260px 1fr; position: relative; } .document { padding-top: 3rem; min-height: 100vh; } #intro { padding: 2em 1rem; border-bottom: 3px solid black; background: #081b3c; width: 100%; min-height: calc(90vh - 3rem); margin: auto; display: flex; align-items: center; } #intro img { display: block; width: 100%; margin-left: auto; margin-right: auto; margin-bottom: 10vh; transition: 0.25s ease; max-width: 100vw; } @media (min-width: 1000px) { #intro img { transition: 0.25s ease; max-width: 90vw; } } #intro p { margin: 0; margin-bottom: 1rem; max-width: 55ch; color: #dddddd; } #intro p a { color: #dddddd; border-bottom: 1px solid #dddddd; } #intro p a:hover { color: #66DD00; border-bottom: 1px solid #66DD00; } #intro ul { margin: 0; display: flex; justify-content: space-between; justify-items: flex-end; flex-flow: row wrap; max-width: 400px; } #intro li { display: inline-block; list-style: none; margin-bottom: 1rem; } #intro .columns-2 { grid-template-columns: 2fr 1fr; margin-left: auto; margin-right: auto; } #intro .columns-2 .button { float: right; } @media (max-width: 1000px) { #intro .columns-2 { grid-template-columns: auto; } #intro .columns-2 .button { float: left; } } .toctree-wrapper ul { text-indent: 0; } #designers, #developers { display: inline-block; vertical-align: top; width: 48%; } #developers { float: right; } #footer { height: 16rem; position: absolute; z-index: 1; width: 100%; padding: 1.5rem 1rem; } #footer > div { display: flex; align-items: flex-start; justify-content: space-between; } @media (max-width: 1000px) { #footer > div { flex-flow: row wrap; } } #footer > div a { width: 100%; } #footer > div p { margin: 0; } #footer > div div:first-child { order: 2; } @media (max-width: 1000px) { #footer > div div:first-child { order: 0; padding-bottom: 1em; } } #home_button { position: fixed; bottom: 1em; right: 1em; z-index: 4; opacity: 0.15; background: black; padding: 20px; border-radius: 12px; color: white; transition: opacity 0.75s; } #home_button:hover { opacity: 1; } .icon-24 { width: 24px; height: 24px; } .rst-versions { position: absolute !important; bottom: -16rem !important; right: 1rem !important; font-size: 1rem !important; z-index: 1 !important; width: auto; border-radius: 0.5rem; margin: 1rem 0; } .rst-versions .rst-current-version { width: auto; height: 2rem; border-radius: 0.5rem; } /* ---------- media queries ---------- */ @media screen and (max-width: 1000px) { #wrapper { grid-template-columns: 1fr; } #designers, #developers { display: block; width: 100%; } #content { width: 100vw; order: -1; } #sidebar { display: block; width: 100%; min-height: none; } #sidebar-inner-wrapper { position: relative; } } .fade-in { opacity: 0; animation: fadeIn 2s 0.5s ease forwards; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .genindex-jumpbox { position: -webkit-sticky; position: sticky; top: 3.5rem; background: whitesmoke; padding: 0 1rem; margin: 1rem -1rem 0; border-radius: 1rem; color: rgba(136, 136, 136, 0.5); z-index: 1; min-height: 2.5rem; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; } @media (max-width: 1000px) { .genindex-jumpbox { padding: 0 2rem; margin: 1rem -2rem 0; } } .genindex-jumpbox strong { color: inherit; } .genindex-jumpbox a { font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; padding: 0.5em 0.25rem; background: rgba(136, 136, 136, 0); transition: 0.25s ease; } .genindex-jumpbox a:hover { color: white; background: #888888; } #content .genindextable ul { padding-left: 0.75em; } #content .genindextable ul li { margin: 0; line-height: 1.25; } #content .genindextable ul li:before { font-family: "Source Code Pro", "Menlo", "Andale Mono", "Monaco", "Consolas", "Courier"; content: "→"; color: #888888; } #content .genindextable ul:not(:first-child) li:before { content: "•"; } #content .genindextable a { display: inline-block; margin-bottom: 0.75em; } body .highlight .k { color: #0062FF; } body .highlight .gp { color: #ff5329; } body .highlight .s2 { color: #267ed7; } body .highlight .ow, body .highlight .nb { color: #008e20; } body .highlight .bp { color: #8800ff; } body .highlight .c1 { font-style: normal; } /*# sourceMappingURL=fontparts.css.map */robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts.css.map000066400000000000000000000225171477533125200335120ustar00rootroot00000000000000{"version":3,"sources":["fontparts.sass","sass_partials/reset.sass","fontparts.css","sass_partials/elements.sass","sass_partials/variables.sass","sass_partials/navbar.sass","sass_partials/search.sass","sass_partials/sidebar.sass","sass_partials/content.sass","sass_partials/general.sass","sass_partials/animations.sass","sass_partials/objects-index.sass","sass_partials/syntax-highlighting.sass"],"names":[],"mappings":"AACQ,iBAAA;ACDR,4GAAA;AAEA,sCAAA;ACCA;EAGE,uBAAuB;CACxB;;ADAD;;;ECKE,oBAAoB;CACrB;;AAED;EACE,mCAAmC;CACpC;;AAED;ECVC,UAAA;EACA,kFAAA;EACA,kBAAA;EACA,mBAAA;EACA,mBAAA;EDYC,kBAAkB;CACnB;;AAED;ECXC,wFAAA;EACA,kBAAA;EACA,mBAAA;EACA,oBAAA;EDaC,sBAAsB;CACvB;;AAED;EACE;IACE,gBAAgB;GACjB;CACF;;AAED;ECbC,uBAAA;EDeC,0BAA0B;CAC3B;;AAED;ECXC,uBAAA;EACA,aAAA;EACA,sBAAA;EACA,iBAAA;EACA,sBAAA;EDaC,uBAAuB;CACxB;;AAED;ECZC,wFAAA;EACA,kBAAA;EDcC,mBAAmB;CACpB;;AAED;ECbC,kFAAA;EDeC,oBAAoB;CACrB;;AAED;ECdC,iBAAA;EDgBC,aAAa;CACd;;AAED;ECfC,sBAAA;EACA,mBClBe;EDmBf,eAAA;EDiBC,kBAAkB;CCdnB;ADgBD;EACE,eAAe;CCdhB;ADgBD;EACE,+BAA+B;CCd/B;ADgBF;EACE,iCAAiC;CCdlC;ADgBD;ECdE,2BAAA;EDgBA,oBAAoB;CACrB;;AAED;ECdC,aAAA;EACA,sBC7EU;ED8EV,wFCvEe;EDwEf,mBCzCe;ED0Cf,oBAAA;EACA,oBAAA;EACA,kBAAA;EACA,aAAA;EDgBC,kBAAkB;CCfnB;ADiBD;ECfE,oBAAA;EACA,aAAA;EDiBA,gBAAgB;CCfjB;ADiBD;ECfE,2BAAA;EDiBA,oBAAoB;CACrB;;AAED;EChBC,YAAA;EACA,uBAAA;EACA,aCjFQ;EDkFR,4BAAA;EACA,YAAA;EDkBC,cAAc;CACf;;AAED;ECjBC,UAAA;EDmBC,qBAAqB;CCjBtB;ADmBD;EACE,kBAAkB;CACnB;;AClBD;;EAGC,eAAA;EACA,UAAA;EDoBC,kFAAkF;CACnF;;AAED;EACE;ICnBA,eAAA;IDqBE,gBAAgB;GACjB;CACF;;AAED;ECrBC,kFAAA;EACA,aAAA;EACA,mBCpGe;EDqGf,kBAAA;EACA,cAAA;EDuBC,oBAAoB;CACrB;;AAED;EACE,kBAAkB;CACnB;;AAED;EACE,kBAAkB;CACnB;;AAED;ECxBC,kBAAA;ED0BC,gBAAgB;CACjB;;AAED;EACE,iBAAiB;CAClB;;AAED;ECrBC,kBAAA;EDuBC,sBAAsB;CACvB;;AAED;ECvBC,aAAA;EDyBC,sBAAsB;CACvB;;AAED;EACE,YAAY;CACb;;AAED;EC3BC,0BAAA;EACA,aAAA;EACA,iBAAA;EACA,eAAA;ED6BC,gBAAgB;CACjB;;AAED;EACE,aAAa;CC7Bd;AD+BD;EC7BE,eAAA;ED+BA,uBAAuB;CC9BxB;ADgCD;EACE,uBAAuB;CACxB;;AChCD;;EDoCE,mBAAmB;CACpB;;AAED;EClCC,cAAA;EACA,sBAAA;EACA,wCAAA;EDoCC,wCAAwC;CClCzC;ADoCD;EACE;IACE,mBAAmB;GACpB;CCrCF;ADuCD;EACE,UAAU;CCtCX;ADwCD;ECrCE,eAAA;EDuCA,iBAAiB;CAClB;;AAED;ECtCC,mCAAA;EDwCC,iBAAiB;CCpClB;ADsCD;ECpCE,sBAAA;EDsCA,eAAe;CAChB;;ACrCD;;;;;;ED6CE,oBAAoB;CACrB;;AAED;EGnPC,aAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EAEA,kBDLU;ECMV,wFAAA;EACA,kBAAA;EACA,aAAA;EACA,cAAA;EHoPC,oBAAoB;CGnPrB;AHqPD;EACE,aAAa;CGpPd;AHsPD;EGpPE,eAAA;EHsPA,kBAAkB;CACnB;;AAED;EGrPC,cAAA;EACA,YAAA;EACA,mBDLS;EF4PR,oBAAoB;CGrPrB;AHuPD;EGrPE,gBAAA;EHuPA,oBAAoB;CGtPrB;AHwPD;EGtPE,sBAAA;EHwPA,QAAQ;CGtPT;AHwPD;EGtPE,cAAA;EACA,oBAAA;EACA,+BAAA;EHwPA,YAAY;CGtPZ;AHwPF;EGtPG,cAAA;EACA,+BAAA;EHwPD,oBAAoB;CGtPpB;AHwPF;EACE,mBAAmB;CGtPnB;AHwPF;EACE,YAAY;CACb;;AGvPD;;EH2PE,uBAAuB;CACxB;;AAED;EACE,yBAAyB;CAC1B;;AAED;EACE,2BAA2B;CAC5B;;AAED;EG1PC,iBAAA;EACA,WAAA;EACA,uBAAA;EH4PC,cAAc;CG1Pf;AH4PD;EACE,YAAY;CG1PZ;AH4PF;EACE,uBAAuB;CGvPvB;AHyPF;EACE,2BAA2B;CGvP1B;AHyPH;EACE,wCAAwC;CGvPvC;AHyPH;EGvPI,2BAAA;EHyPF,yBAAyB;CGvPxB;AHyPH;EACE,uCAAuC;CGrPtC;AHuPH;EACE,uCAAuC;CGrPtC;AHuPH;EGrPI,yBAAA;EHuPF,yBAAyB;CGrPxB;AHuPH;EACE,wCAAwC;CACzC;;AAED;EACE,cAAc;CGvPf;AHyPD;EGvPE,6BAAA;EACA,eAAA;EACA,mBAAA;EHyPA,oBAAoB;CGxPpB;AH0PF;EGxPG,aAAA;EACA,eAAA;EACA,gBAAA;EH0PD,oBAAoB;CGzPpB;AH2PF;EACE,iBAAiB;CG1PlB;AH4PD;EG1PE,aAAA;EACA,uBAAA;EACA,eAAA;EH4PA,iBAAiB;CAClB;;AAED;EACE,YAAY;CACb;;AAED;EIlXI,wFAAA;EACA,cAAA;EACA,gBAAA;EACA,sBAAA;EACA,mBAAA;EJoXF,0BAA0B;CAC3B;;AAED;EInXI,eAAA;EACA,YAAA;EACA,aAAA;EACA,mBAAA;EACA,SAAA;EJqXF,WAAW;CACZ;;AAED;EIpXI,uBFnBO;EEoBP,wFAAA;EACA,eAAA;EACA,wBAAA;EACA,uBAAA;EACA,aAAA;EACA,uBAAA;EACA,aAAA;EACA,sBAAA;EACA,WAAA;EACA,iBAAA;EJsXF,iBAAiB;CIpXf;AJsXJ;EACE,0BAA0B;CInXxB;AJqXJ;EInXQ,cAAA;EACA,iBAAA;EAEA,uBAAA;EACA,YAAA;EJoXN,iBAAiB;CIlXX;AJoXR;EIlXY,oBAAA;EJoXV,0BAA0B;CAC3B;;AAED;EACE,cAAc;CACf;;AAED;EInXI,mCAAA;EACA,0BFnCe;EEoCf,uBAAA;EJqXF,mBAAmB;CACpB;;AAED;EACE,gBAAgB;CInXV;AJqXR;EACE,oBAAoB;CInXV;AJqXZ;EACE,cAAc;CIpXZ;AJsXJ;EIpXQ,kFAAA;EJsXN,kBAAkB;CACnB;;AAED;EK7bC,yBHuBe;EGvBf,iBHuBe;EGtBf,UAAA;EACA,+BHyBc;EGxBd,oBAAA;EACA,WAAA;EACA,oBAAA;EACA,gBHLU;EGMV,kFAAA;EACA,kBAAA;EL+bC,cAAc;CK7bf;AL+bD;EK7bE,UAAA;EL+bA,oBAAoB;CK7brB;AL+bD;EACE,WAAW;CK7bZ;AL+bD;EK7bE,qBAAA;EACA,gBAAA;EL+bA,kBAAkB;CK7bnB;AL+bD;EK7bE,UAAA;EACA,0BAAA;EACA,eAAA;EL+bA,sBAAsB;CK7bvB;AL+bD;EACE;IACE,WAAW;GK7bb;EL+bA;IK7bC,gBHPY;IGQZ,gBAAA;IACA,iBAAA;IACA,2BAAA;IACA,YHfa;IGgBb,UAAA;IACA,sBAAA;IACA,mBAAA;IACA,WAAA;IACA,iBAAA;IL+bC,WAAW;GK7bZ;EL+bD;IK7bE,0BAAA;IACA,gBAAA;IL+bA,iBAAiB;GK7bnB;EL+bA;IK7bC,WAAA;IACA,6BAAA;IL+bC,sBAAsB;GACvB;CACF;;AAED;EK7bC,YAAA;EACA,6BAAA;EACA,iBAAA;EACA,6BAAA;EL+bC,gCAAgC;CK5bjC;AL8bD;EACE;IK7bA,iBAAA;IL+bE,kBAAkB;GACnB;CK9bF;ALgcD;EK9bE,eAAA;EACA,oBAAA;EACA,sBAAA;EACA,qBAAA;EACA,kBAAA;EACA,4BAAA;EACA,mCAAA;EACA,eAAA;EACA,aAAA;EACA,cAAA;EACA,gCAAA;ELgcA,oBAAoB;CK9bpB;ALgcF;EACE,oBAAoB;CK7brB;AL+bD;EK7bE,WHnCc;EFked,8BAA8B;CK5b/B;AL8bD;EK5bE,eHpDc;EGqDd,uBAAA;EL8bA,mCAAmC;CK5bpC;AL8bD;EK5bE,oBAAA;EL8bA,aAAa;CK5bd;AL8bD;EACE,WAAW;CK5bX;AL8bF;EACE,kBAAkB;CK5blB;AL8bF;EK5bG,aAAA;EL8bD,kBAAkB;CACnB;;AAED;EKzbC,gBAAA;EACA,aAAA;EACA,cAAA;EACA,kCAAA;EACA,WAAA;EACA,oBAAA;EACA,uBAAA;EACA,iBAAA;EACA,WHtGe;EFiiBd,UAAU;CKzbX;AL2bD;EKzbE,WAAA;EL2bA,qBAAqB;CACtB;;AAED;EM/jBI,oBJeM;EIdN,oBAAA;EACA,sBAAA;ENikBF,gBAAgB;CM/jBd;ANikBJ;EACE;IMhkBM,aAAA;INkkBJ,UAAU;GACX;CMjkBC;ANmkBJ;EACE,eAAe;CMjkBb;ANmkBJ;EACE,gBAAgB;CMjkBd;ANmkBJ;EMjkBQ,mBJNS;EIOT,gBAAA;ENmkBN,sBAAsB;CMhkBpB;ANkkBJ;EACE,iBAAiB;CMhkBf;ANkkBJ;EMhkBQ,aAAA;EACA,mBAAA;ENkkBN,6BAA6B;CM/jBvB;ANikBR;EM/jBY,gBAAA;ENikBV,mBAAmB;CACpB;;AAED;EACE,sBAAsB;CMhkBpB;ANkkBJ;EACE;IACE,uBAAuB;GACxB;CACF;;AAED;EACE;IMnkBM,uBAAA;IACA,mBAAA;INqkBJ,oBAAoB;GACrB;CACF;;AAED;EMrkBI,cAAA;EACA,+BJjCM;EIkCN,eAAA;EACA,iBAAA;ENukBF,oBAAoB;CMrkBlB;ANukBJ;EACE;IMtkBM,mBAAA;INwkBJ,oBAAoB;GACrB;CMlkBC;ANokBJ;EACE;IMnkBM,uBAAA;IACA,2BJjDE;IIkDF,kBJlDE;IFunBN,mBAAmB;GACpB;CMpkBC;ANskBJ;EMpkBQ,eJ3BQ;EI4BR,uBJhBQ;EIiBR,8BJ7BQ;EI8BR,yBAAA;EACA,mBJ1DE;EI2DF,cAAA;EACA,qBJnBW;EFylBjB,uBAAuB;CMpkBjB;ANskBR;EMpkBY,cJzBI;EF+lBd,8BAA8B;CMpkBxB;ANskBR;EMpkBY,wFAAA;EACA,eAAA;EACA,wBAAA;ENskBV,6BAA6B;CMnkBnB;ANqkBZ;EACE,gBAAgB;CMnkBN;ANqkBZ;EACE,gBAAgB;CMnkBV;ANqkBR;EACE,sBAAsB;CMnkBhB;ANqkBR;EMnkBY,oBAAA;ENqkBV,YAAY;CMnkBF;ANqkBZ;EMnkBgB,aAAA;ENqkBd,aAAa;CMnkBH;ANqkBZ;EMnkBgB,0BJnDG;EFwnBjB,uBAAuB;CMjkBT;ANmkBhB;EACE,yCAAyC;CMhkB3B;ANkkBhB;EACE,0CAA0C;CMhkBxC;ANkkBJ;EMhkBQ,UAAA;ENkkBN,kFAAkF;CMhkB5E;ANkkBR;EMhkBY,eAAA;EACA,0BJrEO;EFuoBjB,uBAAuB;CMhkBjB;ANkkBR;EMhkBY,iBAAA;ENkkBV,kBAAkB;CM9jBhB;ANgkBJ;EACE,kBAAkB;CACnB;;AAED;EACE;IACE,kBAAkB;GMhkBlB;ENkkBF;IACE,kBAAkB;GMjkBlB;ENmkBF;IACE,kBAAkB;GMlkBlB;ENokBF;IACE,kBAAkB;GMnkBlB;ENqkBF;IACE,kBAAkB;GMpkBlB;ENskBF;IACE,kBAAkB;GMrkBlB;ENukBF;IACE,kBAAkB;GACnB;CMvkBH;ANykBA;EACE;IACE,kBAAkB;GMxkBlB;EN0kBF;IACE,kBAAkB;GMzkBlB;EN2kBF;IACE,kBAAkB;GM1kBlB;EN4kBF;IACE,kBAAkB;GM3kBlB;EN6kBF;IACE,kBAAkB;GM5kBlB;EN8kBF;IACE,kBAAkB;GM7kBlB;EN+kBF;IACE,kBAAkB;GACnB;COjvBH;APmvBA;EOjvBC,YLwBwB;EKvBxB,iBAAA;EACA,kBAAA;EPmvBC,mBAAmB;CACpB;;AAED;EACE,kBAAkB;CACnB;;AAED;EOjvBC,cAAA;EACA,iCLHS;EFsvBR,eAAe;COlvBhB;APovBD;EACE;IACE,4BAA4B;GAC7B;CACF;;AAED;EACE;IACE,yBAAyB;GAC1B;CACF;;AAED;EACE;IACE,yBAAyB;GAC1B;CACF;;AAED;EOvvBC,+BAAA;EACA,cAAA;EAEA,iCAAA;EPwvBC,mBAAmB;CACpB;;AAED;EOhvBC,kBAAA;EPkvBC,kBAAkB;CACnB;;AAED;EOhvBC,kBLrCQ;EKsCR,+BLfe;EKgBf,oBAAA;EACA,YAAA;EACA,8BAAA;EACA,aAAA;EACA,cAAA;EPkvBC,oBAAoB;COhvBrB;APkvBD;EOhvBE,eAAA;EACA,YAAA;EACA,kBAAA;EACA,mBAAA;EACA,oBAAA;EACA,uBAAA;EPkvBA,iBAAiB;COhvBjB;APkvBF;EACE;IOjvBC,uBAAA;IPmvBC,gBAAgB;GACjB;COnvBF;APqvBD;EOnvBE,UAAA;EACA,oBAAA;EACA,gBLpCc;EFyxBd,eAAe;COnvBf;APqvBF;EOnvBG,eAAA;EPqvBD,iCAAiC;COnvBhC;APqvBH;EOnvBI,eAAA;EPqvBF,iCAAiC;COnvBlC;APqvBD;EOnvBE,UAAA;EACA,cAAA;EACA,+BAAA;EACA,wBAAA;EACA,oBAAA;EPqvBA,iBAAiB;COpvBlB;APsvBD;EOpvBE,sBAAA;EACA,iBLlFQ;EFw0BR,oBAAoB;COpvBrB;APsvBD;EOpvBE,+BAAA;EACA,kBAAA;EPsvBA,mBAAmB;COrvBnB;APuvBF;EACE,aAAa;COrvBb;APuvBF;EACE;IACE,4BAA4B;GOtvB7B;EPwvBD;IACE,YAAY;GACb;CACF;;AAED;EACE,eAAe;CAChB;;AAED;EO9uBC,sBAAA;EACA,oBAAA;EPgvBC,WAAW;CACZ;;AAED;EACE,aAAa;CACd;;AAED;EO/uBC,cAAA;EACA,mBAAA;EACA,WAAA;EACA,YAAA;EPivBC,qBAAqB;CO/uBtB;APivBD;EO/uBE,cAAA;EACA,wBAAA;EPivBA,+BAA+B;CO/uB/B;APivBF;EACE;IACE,oBAAoB;GACrB;COlvBD;APovBF;EACE,YAAY;COlvBZ;APovBF;EACE,UAAU;COlvBV;APovBF;EACE,SAAS;COlvBR;APovBH;EACE;IOnvBE,SAAA;IPqvBA,oBAAoB;GACrB;CACF;;AAED;EOrvBC,gBAAA;EACA,YAAA;EACA,WAAA;EACA,WAAA;EACA,cAAA;EACA,kBAAA;EACA,cAAA;EACA,oBAAA;EACA,aAAA;EPuvBC,0BAA0B;COtvB3B;APwvBD;EACE,WAAW;CACZ;;AAED;EOrvBC,YAAA;EPuvBC,aAAa;CACd;;AAED;EOpvBC,8BAAA;EACA,0BAAA;EACA,uBAAA;EACA,2BAAA;EACA,sBAAA;EACA,YAAA;EACA,sBAAA;EPsvBC,eAAe;COrvBhB;APuvBD;EOrvBE,YAAA;EACA,aAAA;EPuvBA,sBAAsB;CACvB;;AOnvBD,yCAAA;APsvBA;EACE;IACE,2BAA2B;GAC5B;;EAED;IOrvBA,eAAA;IPuvBE,YAAY;GACb;;EAED;IOtvBA,aAAA;IPwvBE,UAAU;GACX;;EAED;IOxvBA,eAAA;IACA,YAAA;IP0vBE,iBAAiB;GAClB;;EAED;IACE,mBAAmB;GACpB;CQx9BH;AR09BA;EQx9BI,WAAA;ER09BF,wCAAwC;CACzC;;AAED;EACE;IACE,WAAW;GQ19BX;ER49BF;IACE,WAAW;GACZ;CSr+BH;ATu+BA;ESr+BI,yBAAA;EAAA,iBAAA;EACA,YPyCY;EOxCZ,uBAAA;EACA,gBAAA;EACA,qBAAA;EACA,oBAAA;EACA,gCAAA;EACA,WAAA;EACA,mBAAA;EACA,cAAA;EACA,oBAAA;EACA,+BAAA;ETu+BF,gBAAgB;CSr+Bd;ATu+BJ;EACE;ISt+BM,gBAAA;ITw+BJ,qBAAqB;GACtB;CSv+BC;ATy+BJ;EACE,eAAe;CSx+Bb;AT0+BJ;ESx+BQ,wFAAA;EAEA,uBAAA;EACA,mCAAA;ETy+BN,uBAAuB;CSx+BjB;AT0+BR;ESx+BY,aAAA;ET0+BV,oBAAoB;CACrB;;AAED;EACE,qBAAqB;CSt+Bf;ATw+BR;ESt+BY,UAAA;ETw+BV,kBAAkB;CSv+BZ;ATy+BR;ESv+BY,wFAAA;EACA,aPDI;EF0+Bd,eAAe;CSv+BT;ATy+BR;EACE,aAAa;CSv+BP;ATy+BR;ESv+BY,sBAAA;ETy+BV,sBAAsB;CACvB;;AAED;EACE,eAAe;CU1hCT;AV4hCR;EACE,eAAe;CU1hCT;AV4hCR;EACE,eAAe;CU1hCT;AV4hCR;EACE,eAAe;CU1hCT;AV4hCR;EACE,eAAe;CU1hCT;AV4hCR;EACE,mBAAmB;CACpB","file":"fontparts.css"}robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts.sass000066400000000000000000000010621477533125200331070ustar00rootroot00000000000000// fonts @import url('https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:400,400i,700') // Sass partials @import 'sass_partials/reset.sass' @import 'sass_partials/variables.sass' @import 'sass_partials/elements.sass' @import 'sass_partials/navbar.sass' @import 'sass_partials/search.sass' @import 'sass_partials/sidebar.sass' @import 'sass_partials/content.sass' @import 'sass_partials/general.sass' @import 'sass_partials/animations.sass' @import 'sass_partials/objects-index.sass' @import 'sass_partials/syntax-highlighting.sass'robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts_logo/000077500000000000000000000000001477533125200332355ustar00rootroot00000000000000favicon.png000066400000000000000000000003461477533125200353140ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts_logoPNG  IHDR szz pHYs%%IR$IDATX K Dh]tV,|؄L`f4"J8a=.aހ9p݂y[ d 4Pтk[oy)@7S-hk]`]{vtx-p ~n h\wl}IENDB`favicon.svg000066400000000000000000000021231477533125200353220ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts_logo Asset 17 fp-monogram.svg000066400000000000000000000020661477533125200361250ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts_logo FontParts full_animated.svg000066400000000000000000000336561477533125200365200ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts_logo fp-logo-lines full_ltgray.png000066400000000000000000000553151477533125200362210ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/fontparts_logoPNG  IHDRjN pHYs  ~ IDATxQI0bfHkX X ! `," .Xp@D4H]U<UWW~'rί;n`.;K)Оsw];\/U}Lc @a}PY]B=E(h!*~Xx׏f%t2ȑhWv@9K)_OjtؘlzkZ*~PK:si:J %'`xK\_aAaK@Ibzk&/^jB\Jmnv\r W9,H_<ఱn!T86$nl)_Iq͂nUJɛ.`Ab#aI`;^=?m#t)G!'Y:+ي{am (T.pW5pKs@nvj;^b X`JzƇ76~ 1^:;%,dw(OِD¾??f409#\R#@Yʹ(Z.]'o%a8"uſENPW;3)`~{w= ;hSuİ;m/kx@Hf@r;qp9EK)' li3@@@@@@@@w{љ/)K ӓsݙ5uJ麁'H4/0Ӝ2wWs>78t]9睈Q۞qS}企k!Dy8s`_ Q;x̞" #P{&H̀9.|4tumuXb ɿؤ8k:'39&r}X8P9ي{,^ŘF^<9:oMorR)~o XX1 ·sW6+H̉C,߿xp_ã o{tԡ17N4q띆7X)'xoMq#Xk?8T˲e"'@K&^eQk@< /58uk")i9LZѯmr$ggyNqt?+Ӳ`>`F`F`F`F`F`F`F`FW9n] ΄|2t`zr;.5=ǴKB\FNu)mg hoꗻoFL]׽sNRJGڗsލn8뺣җg"GK,瘇.5;Ssދ)²}S9t]o r՟y9v]_Mۈcl(r~#boK!t@r'k=d?,39'R}8"9|@~)vEJiO@;Ġ&aAi=!Zm"foh+w3wo7O?)} bA564rzOx@K.Ƌ} 7)%p^LS s_6$΅ g,FfX_AH(s7-9/bJ)kxfV?vD1&y\,ȑQbl@RJ^"r X}r{C9}zY~467(t$GlШConx?ߞOz%4&b} ſoHAOТM=99[=<Z#0[JP@"aG}T nФ"[F?)`~ &m=qJC zߴ_y=m9+Y`nb:a&缿SZh=ZiK|(((((((((뱯s~u~u{]hcu^3<,947"|uauݍԦ C9c8۾/|3rFP6p9OR:j".g.ŢE+ĘEM$b1qKk_wA/xEYttQ? \SIyy+k8yf`L}@Ν\LOD#>F6p)q՟jxz&̇41Poں2&6Q7^7x^b;1 6b"oXGNSJKh ^[M֕CL۫3ſl1A(?A6ٮRJM*KI~/%.l`"k0)>&Jz*Q wMONsJ8 /Nмi~_Y;a-QŏEXyr}jDR*$E_Wc6rα;ryѯJ9;|DC^lj{*Grh^}{+ulBs,i{3vPqW?mg`SoF:"v(3 I CSI)ͫ#jڿ"NʀEj5ooSE:{"u vډ؎y} ׿Ŏ:1&CZ*֫b( -%7oo@~80vkR뼼!ޟЬɾ?8U7g"9دߣqosdcWmY W(S^cb͕Xs db]O?h"n雁@yK0Dӭ-}_gڼ165*esטus%yy+Cj?Xד}{-/6b-m @]`iVzA9|DZȟK_pOE_P9|1(n:/oeHsq+zrT,4FiC^`LJnMNn.!}|7^eⓢ>%u1͕څ!ޟĭɾR:&T.HSfA Nu6s793 )*&r [S(bƇ{:_>p4'yCj?ɡYד}7{4HY/ϰx›#n}Hp?y 3v#Qc_> ߬#G(zW66k1M#6Į+[ЧȡY}0Nhx:CcT9d("0ڟPJ@w4VfCc# jzW4Q ǹ-%fm+s1Ay8JaM o?<w n1F:̤yi ڔzIdj0}?5%ci1ɑ1zb+6k_l&$ !'T,6+:ogCW1L" 𬾽b] ǐh- N6) '߿66E?1^4r5XNqtqmZysw{>S={~:og }_^Է{9GNHf.Z_d\+?#'^q%MrίWnԺ5k4?gi~mdqsލvi$Σ]eV4vM.~^{n6:o'.WY4V~Lϖ6PQQQQQQQQc_%|u~u4Zn;8t9맢NuG]u]StR:͎tהEI9 dӮV/-ݧ^o9zbۏޭfe^@9jbv_c6aUn~(wWr u9IĝM+Ğ~^ܷ">x`КO4Ar]9l槜s!.eH}_ I'6Fq'!|6FљiP ֻF;9#R:bDd>JH'Tx`l#s>>:3 C{vZ\2ȃϝ:r} K)6R[bNSJc9RQU>]QƩckW6_botp}c,ԯ}7WNJlx~xplD7u$k3t|o"x,Mur ֻq:SR '!5+~͋},MUg}Pנ.?3ī$Xn yJliNtx'9Ryb1;5a3y=,J65+_n{5KҪAY?jaފdZ`]G3 -o[L8v7[m`MW|9RUETRtIq|K*^hop}eeG*'=Vϛ!OE2=NѢ)OK{vߞ^K Zj5}1g{xOКw#=K'"w@r3/8C-U8gɾ4u ˦idփ I0w6ijPRC-7̑ue=HQЪT^S:ߒW:kujj{r.UjJAo)6M)631oe*kYR}ɾ5{U Xf7Q_~b]%c'k5\" Փ}{KA: ѲLeYRJKL^?Gm2qu1[]%$]=)!6U% Ŷ[g՚'('߭`:e~m Al)^$C!'ș!c iOVH)&]lp~)5kq8TRVPRT\2I.6h%WSLt"]a֥vLt{ >Łs_wm|Y;IN9(pwhk[;gRs>3Pя}0Nh WNa"@#AА(|bRJN5v эDQTN/Q]9WʞD1 Q0sUMbM_rI(AO2v{{Ozxo:do*6vm2O+\PRh},ЍqO:59R1c]1-/߹#gbD}۝!XZ(5sn~;1~KcZ.blH=9F`U"o)'9Dwbܼor6UMb?'=ߺ-朏w9mWCxQ*6y/b3Ga"iRu9fA(ݱ>!CxHlr55Y6283QA:&v&o:徳QU~DfC/?,Ӳ`>`F`F`F`F`F`F`F`FW9n7vI)Oz(0vw])˩7qĝGq=aXr5#wbkr2rBccrì];6e%^+s{1ߛysj}Bۿ}/":muփA9ĸviMu')5q\yjCr1I+7T4sؐ?#7NU9/4sSF;4E(XƟW STM06muAb3fSJhRHz0a:9RH9 pmYdFDR:nRs}.yu$3=XZe$_ =Ee s^f1w)VqHaWG9R;3-MQ@97SHZed?[Ub[N?qf3{eK>Nq87HUˑbU+M<73iL9.;>[i- )HNҀg(x0 s&ss3ŃDdRl*m@T^)WDy0Uc.ƲI;D*(S뻴}{+i7u(.fNZKҟ(lIs~zP3s IDATTG=p*!Rі))LьP"/E ~ط'G{52EiUMl68cZ5p ,C/GՐO=ͷY"T`Ƙ{]}0I2<Cfɾ]{y.%joSj'~i3`|al{o.-y?*gXP2vkT j2wiɓ}/kg,EckR. ~Zks笥˵z^2軴ɾ%R.>0/mg,TS1DJ8!UrE[s3'Pcȟ:u1+4U5YWFqҴCj9|*#;K6ȚRp 5#.cl/xZZ'>OyooEpɭhߑhkb$y:!5Hon#w̕7Ws>WK9ƧOHw͋\/-m`Gٷ.g h(8v2w ]Th-)h c-<1qۏ ,c9 n"v^T>OϞڌv䜏b&Q6JZFm3 6Cg*ogeco 9Ok ùzm6?I{}tBc%6e7cFo(ܞ+|rb><967Rh3O)ZQ=bNSJ3n"|-JlY7, iaZQ0NoϜv|3N({"c}ͣ_]EɑRP>(E9_;VPfQȞLq 5o"muO[M<%4$O; =6 Ww߆V^ofUgBfFiC㰊7z~*'[HeqXiG6Sh@2v{ļEiAUi3T>1/glV\MP k,loͶ߳q0ތІ~mFTE׀;^/=,f&)=i'SQ{ѷ'[mܰ$CN =$lCcpx?06fé݆6>>,6Hu/Jm8!?hnų'Svxq}oAhPai67a!IֻZc8G^X ũeڬ ,I]ӌ͸Rd6.9Zvox5V't!3^]%?L6yIݖ`<\b-6TWxLW:2JWmV,I]̡qR:yuOtU}ZsgfëF1ׁnáT~SEF9RWhÉ!wo)lxlTuu'f\_?W*.77~#XӡHOUIqS(wu!ø OIdi c(췟&ĦawuؓC8pzci F΁ e=cc&f۰i|6?.64JU\I'He}T>rpo0ڌ|{lPU {2xwJ󾜹qOཡxa#6>*Ї1a caݥ$&?O'57r;*~sᵮ/6g-@ly/qiMms]k_flJL?As>.e.M҅ޜs?Ij')˚qȑ18і[e򽘛Nbfn"Fkf9X0'sՃ.wx<{2'37b&1̈́c{1cӔDQ1Vl0};_w6?wi.~SqR:)I86FE7nKCR# #G: R"*tR\tX:ޓ㚹[5/uSz@)󈯓;(U1Z6g˫#:hQmMh2CR$K?i,mŸ.=:? &a}025J9WcnnFW(Cf^Zԧ17[،HhJ0:1QK)ZGX{6˹ؗٓԞ̬DL+su?<$-џ ګxE\D[K{N 6"'$W G61*b [2~Zd1F"Gb>54]Yf!СǺ;~ {2ݓ}0aߍŮNNNc\@WAqſѧviբfRJA+Y\11-ܩdslmȑcyX#o0UcJ):}W=UٓI$LУcGJuLZ-7⾓sމj{h_=sދ6irKXQan/s>f=&Ơ8eD;^kkn-9wqxWB&rSD,jew2і^8{2k}Ofn$4c 0=[ CfDfDfDfDfDfDfDf_?*9=Ma)/S9]6p)StRns}nR&'t>5wۙϘcpm_RJc^qFK!G1e]gA֮Mbbk-_1hrwwͿqu]׽bTsSYb"?}3eW'}Qu]m7p9SuuY?Vjmd6;n;Xn!GۈkbmgԨɑhMu'簮`b@֑D\\ٓD=%Ѐ'7~$:*cR:lG.eNA IwOl\6DovcL7~ >nGJxm2HLHIʓGu7ʸCYjA]D[*bO{2K\@#_ qR:h1r \]h"Ť(:KV4*Z4kԜɑ*?~ƺߨ:.]Ew1W֍]x.>Фch1͝vX/|i"IoGZQ@Տ9dٻn}Ks?q;kb*U\9иc~+Xg!G1)ލjWfYbLʫ1C| vo2)5Oj;~f,u^X.{OY2{2UԚDC˾8jmBv&:ڨ6֜x%^Ql-"|¼/p8cFS]r$f`9֙:2}pWsѠ6uEWOu Ҿc.vJqH]o:U]?`yں?Vru֥Ce*vtX17?uYs@~òܿRӠ[XוoxeC9_&ajPu9Џh}oXK@ZRQg=)[5ȚRtP"|]zu1X:X1bURr!KN1 /i.R}bbJn݊%!ofurR-qE9~τMq7Ɠ=Yԏ<׵"Qc1xmqA41^Mr$sqI2v,D32.?ou4w1WdQ}Of;1h-333333333򯧾Jyݮ4|뺳儮_w]׏c;+c亱k*mu#Պf0缳n; \ҔR:kᚍ5hg\i/ő#MēVⷝ_mRcgeYp,\Y6џJ1|17"nqu5r.;.Ko^-^ՍU|OTi`tf#w-[: kXbnRXlc)xXCߌ@iqܙ9ŏ7Kkhzcalp("TU q|EYh^UJ+F"G*N@KFm ≜:&Z F9)=1_Uoz]ea(3I؈[O6vճ=r~ eLq} ljP3&V []%9mYW5Kl2o,ѰƊ̑+fڥ.yLYb2W|ݭR3OMP /^joys=Wcnu})iڲm@25 \duy˲Z)R>{~Ζ~t5gúI)_SJ}uS-Y$ARWMo%>^Ke͍Cc| &rR2Eַ]nn՜:r91&Okles8_,WrR*ibEIHb.Eu˕n3Q=uab"nԜ_~?.~OK166ѷq;TVcÊMUc=yARJ'#c;Gb+fqcmwr?X̻/w? )c(Y;n`16?3S rsGrU#IH9HQiXϲ5*E[c'ǁ>߈ᢩf&mbi"_=ϱR2_ZXLDk 굍ֆ 2lVrR:^L_^,]n8{1v2>وZ()4E%jmfo^X}vx=7ckRJ.F:_c[ =|D<3"a|ؠ>Qtߌ<GKFF|/ro0Gwo/rcM܈PV&pN?WWo6JٗS01ZߞCuu0p'3N-Wyge ^xD?FNZ~Sv5ن9{WV!k8 m,MC%Gum99ﮌ-9WʹY]&vkרo,/bg^)%7~8-Ӵ`>`F`F`F`F`F`F`F`F`FW9u]wu]m bW]םu]wRصSXyge|ui2!BsR:ks}0u,E{ sߎuQ)LUk9Rr$% 'kw.k|Xlǿ3J,NV1 <5(>p{u/:IӔ /s'd**nCʹl C^J|mn*JYSc방{[?tzsLڝxzalB-h>Gr@Mr|#~/gXۅO69_|aj5jLӨ#59[?)e#?sDZDv`ul71J{rjH*ĺk-풯c5vۡ3ƴHMo}PU,%{v l=%F^VWKPJg]7e DN˕,yUY3«Ym 3SAhof'twjd-j);oZ WU_WCST߫h~ RJC#5=D_<,40j/fs _%eM ~e]mӄ!Mȑ'{egM#I5! dg(P\h*vgNZV7)RicyNSJ͜HL)ye(ȑO'QQbgd(bvU֮vX3PcL|VPw#@XcA83q Hp=X^H\s&{GiAZTޭIk֔}*q(8,R?Z|#%G"+9xiX)a`--fnHprm\;[/l"1}Rh(LLZbcdW6Hha8o'n#_GdnmG9hZ^&NUDJ.ǎyC/ю֮ yhcZd>YgK|((((((((G_%uk b)]3 9t]M(2en79ܯ[ \䤔[fsھ.'z?skY$bh9HTg`$w֮2g0sMƜ?Ql畖YUu)^?#9u]wuݶO%r]cM3U]םq̀\8Nb|O~"}^}w%G27m&9X3h뙱0Xf}+ 0}s .itJD 4zf,1ٜ1я݊mܚi oɉ2E\cSq{SJhw`ṪZE@saQ1'W.qwHEMvnM Vu3\snbUa0X~kXyOV7x[Zfs{ͅpJ'>el`=7)%%lX+ EA<ǪM )Ϡ1VN5M >A/:.*lj:&scs̉]Tﻱ9 Gh _rb͸rY:/p)+ǜX ظn?MNcґd|ʫL:?5p)sv1BQ*&QQ1 W1G7IQr$rNҸZ&3/]>\qܚ!&y_ qO,t۟wcs{6iD d "XE`6K"0D`Q2 :\B {/}/RE3}YԂ{K3]aݾ u>/ŷM,mэrlDS%8GݴHWba6Sy/7 *MmwS;9ivHΘAgZ_i=Y9ԵJ5rN9 [0xR:Wj/){oƏɾ_C){`'[/; g5eYL\RڋSG}l59ҡشf~3hDx/̢9E8>rѱ~{V;ylw7J)M'^֓]I Clf+L2HEc%7|VO >9"(xgz:tyDvr+ @ @ @ @ @ @ @ @{fC p1J) pVk9^) <}N;kYLcׁ,9Ŵje@SAT}m_s8j"רѷ8?;.)IU9ǯh(p߮mmƂ|_ 4T;szK y$jBjpmhCN-9s1yu9/;?lL_^)Sm_hd8 gy^c~PV/3eR޾ꏸnԒie){Pyy*?Re0S69{YʛҴ9JN+zWRb^h_@8'^v\\q|yWdGb HR/D)`NGްwDzOM q0ڛ}#zu9^-tOǮMq욥 <$yD. fSl-E`,:$Ll sFB]Ӳc~tR;-<3x,_j`kVܻ!iR{'fN 0I^5?RJ~Z 㶎NJ)`Yᙧ;#/\uU>6 .t-ςR,:r ͩc>k7ܭ<;j=50 9bvC90rĚwtzJ50CcFБe<}@+|)8 !|z;|/wq~)=cw?N`$N471}ǮɋZmc ܷtXګc'.l`Dnc = u9 = Cv;v,dN ;/5 z< iX``re~ ٷSo`lʫYJ)'{XqnH)z;z^:j=wPx:K9BǺ^ K/|zk*1#֕q~ߣ:N@ϝ /@:61>Aȋ:K)7c /oG@W.b;kvmtꮼ^}tʃ3% M(b(U^'iCyCݶ׫ZPJc|}G"GyTKJ)yh\>9s\Ai\ KI;/_/So RS ;q𸨬Z@ (_ 3G_,h}j5/ԕ;T5x\T R4fpSB) oM)y-B4(nKb?0Uǔa(S.V'%DQu%sMAS.^Fc F +r|Jk/So 5]ܾk)2 hC cgZ{RJ9pcP۷GE"С@8PlnKܛ˫g:  O==x҇&^*x,+grz⮄Ym)x̿5@+Nc]7c /L=581Ry@uo+#c_TkBG z(tΗ܉Ww40 [  Ny%*c_`DaJ6AX>O ܧ6N j~>R4^̓G"06$<#c;34t"]>SJ)|; >SO8 )W_-as)e^? C2G)IFC2/Yj\NJcyZ-Ƹ+tn|mPe} ê;ݏ'vYv9hq)ݏk"ǹvyБ|7͉R:,|s]j^{q!!$rY.æ]eA;R+φ8_5x\R-l~l"ƫㅞd\[`Jr0XLJZ,CT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT&41B}@RJG1cW27+Aa|}́݅.BgK @J8'421^iZ|UǔR. BTf13&dԓ&oyr:+uAa:|bFc Fä88zC8ԁ*!X qBT YSҁ&KY|gBG`93xkjX:5ǩrg<,EJvJDz:E}N)d`:^ ̝9&￴ 4ȱ;+Z#G{af4KGXMStڱ^a|P&@>??5 N*-b-58ІGc.ڠ'Z=ƉvvZW2;vrq)S&|hEQɘ0.,$kh1!x,ۮ?ERJ 0rNJmahӇj E N0qܺUg Gc3vZ@" ;Z`Ԓ5Ѕq~" (W\K`IwJ ta<]:aLeh>Ѕ!x,huhVLWKzkU,m:ab ,6(xl>Ц ^pg`Ll':ciey;@ NۮRdZ^sK=ІGQ@ 0nGxn {11; @Uq#T:aסR[ly<ShF]b6@Y:z;8vt|Rw`;% Cqם0 Ba)5oX۲_)B~{Go+܇ot+C2ÕR D3} +8c)e[b'!]Q8rBa5>oyXuҎEec.?^RJ'۝u߸ݏy7y:Mv8Rʗ񋾀9/慮`]Hr$]VG6$廹g7ȃBgxK;w\i1߇ɨ>߯<.J)} !얢q|ÔWׅUmP:r_C'rwSj@W|?yobHLJZ,CT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT IDAT4Bu0X?bW5> U.n&x}oR:!0@{U9ХE1 Ps-yPOFn^KΚҌ~0y~ǥ@P',y0:RJ!C 0yQ{췲|&| @' D2rczC8uW'~)\äm<_+xL)},p2~:+r\Dwer{AUN4~ЙՖ ̖&j< rxYt%h n@b7zF[+N? 0JY^j`IybWؼ>˞l~oR ! >;x^J)? o.}` eR;7޵xE~t[kʑ_K (/`O۷0?grNv<.J)2p?0yyx(p`Y)yGS`cc?~>F39^wv8'IwF vW{RJC}86o<Ε tNR \ @MekFa2kJ;|&|Z(]^.-_mцA;s1 }!l TB-kG$ ]#/VB]}||^XNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#Pm4iJi7Q1^6RuNA12߇je}oXЅB.BgHR:X%46zn}ۅ|JXVCG<0ljH)zI8/Lw[Jiχ0 Ue:V ¤q@ :RJ4i-gDz1nC/2 96qG"G13 ҭ2ρ㿦#rݶ|Y^2>_H[)AxŇʄRJWBG #eW4-2ޑdžXw:<8SOg^<*-h#Ljʮc  5x[? ' =60nHt3Ѐo1Ww2߿hma|GBG!޻a+Z/M#uv,t1ІGc^Q:rhE|h×CH0>v-Wc|3c\  D@,vHynGMZ{i h౬yhwI-6 KH)-Tƞ;@aLڶS*m@jyP`g'ߧ.< wuЁ=0 jI+Ɵi# G+@'F0 &@W?o(G=ta2:f >w]1M0 t&?(eLlxfK @u9xѬlBďҕ`Mo1Z:c @b?4<]v-g~Y33ZhagXm"ІS"(m93Zt8z()ƩxiX>дR,A' E1hН9zzZpt/ؔEaO嗭v5*Cf֔Rʏh?OZڂ 4P_1/b@eB*r|1x, @M_cWZ`b(*5G䎫5vk;`]7 g`rn6av5|{kRJEWkʡiJ)~~5qəfzwǹ N#ĕ`K 36|1=9v w5cKJm۲wZ/J)3Ge7ܕe#)!w\ӱ})|/|xCwaR.`YOO\A׬ 6FJi{S֚<Ε1~ɻ-E],Grjvc}Vhq΀v[&3GaXG]s,ʁI¯@äU`q8Wv@οq;/aZ+TyXv4my;a0߇ɸ+Y~qQyf^xA/݌yj4M+ D)Ob7 qaT||7PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#PNT'x< @uG:#Pm5iJi; /x|ZscRsr_>Bkr!*~6߇N]YGJ6U~2.OC3!d-B;SoDBn<|}0v!LO> V^9)ckcJ)> 6w4dRJG@O +)ݲukjc<Ң0߇Q;ZY)xL)V=hB*?0RweWyZ,+Li&Zo*6Sv-h&K|k  BtOcsaZ  @/虣ҶNY[>bi?;Ќ&u.7v;S<06%xQc>x$tǾԁ|(`u?w<@%4qqR6ϰ!-MOGsi+`e۵=Ү%ԾCwр 9n@TcXEՌp\WU~ԥ閤%u^%  /h>Vv;Uܭ4xn`렗['φ@/NVn69筈(Ҧ>` jC:9睈ϐLvYEZ#NԪOC|v`VR+r֫] R]o G`ZZJ8_qIhaPګM&"#9g>VaS@}:sRRsw<ӱ@sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @ssޏًu UD\GeD\.t˼fMA9烈6Wй8S!s·q۫\X7%K)ꒇ< #WȣbѫǜV)%~8/qRi 1pG3 c("%t'_^I{Qcιl8W8K)M6eţc&n:|'Z"⟝?SJSGǜND\H(]凜ҜOm>:@35scso z?jsފkՎeNJv{H+Y3Q:qR{S/V<`6pb8OXco[s;5x7Kf=?LͨЏQgrq8[gII)ݨ41NXW9ў@s%x֬Ћ˱6lVލηZ05Wcq%F̓NjfyS3L[Rv#tfrNcow䜷"z"I)ݎ[x_;{q'"xM18dSv21W<zޞ Ƨd7~ݸ_њT xED(RR>j(|x0v?<$=$Cxj=N ocqζkxIŠojC}Zd&:k*reu~^Dn~ʋ+RJYٞ^](~`J>RR\y+"ʾ#[GRُk<.9m'`JXP#U7L15Z:: iy"x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'xmМNDG^lEĮ_tY?)k 6LfY90""b{zM DyD!e1\*#҅F_oG)K^<朷jǴ 5Z IDATV@౞x GUwDx1:Fv9^^< ñkK>  qM:0"nO!U=QX_#c]5C[5>c%ط9筈I}s&%Nwg^(`lr }J)]|.q}qQy/"i1TSJza-SpyuK^Gx7͚|ٜsDaD h]EiDu3\u[(ݕydKbq[:x\ ~[') ][\XSBȓ:`< /~0y_#^CTHzSs.Q|./&{F՗@v=胱`@_,dy pn#g59gg> 0P$I$,I#@s%xլ,ZpFJR= 0|ro4@RJ^VXByÕVsI/56}0@=G56u#xӁ8`} *:tR0tk5xmKq9ةXo㉾:d0")[J@֝{c=:P&c} t"6l{DU@P]>ӷ]SX+LUR0nU;p1 `p`U,xժOX/AX g}!`RJJyW*dܖk:Y8h0Ցj+R+܍@+b 0 rqN mO`V<Ӵ@sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sIs{Q>;[6".#5t By!xRoE9$Q8Տ.3s++|+]l#@m-:oWC0#FqQ`x۫/T>Lh@#@=YXo:-ԅ &(t[/`` n:-6騷WHhCXk SuU m+P@k`Xk*|0۩3X*Ot B1"9# 0 ?9C[ۮc 6`Վ0 +M.kFV;׷T= NuUda 芹$,V<.sF$缣жK bQz__R5xQ@1#'cx|ї@nRJ*ط^0GkZ`4@׌;knc @h 0@N8z[ O!cŤ3} tGX M=qQ}t"6l{ctX0BuQB6jeB<ɢ Uj0jX# 0 +nVD`RJY+R]f0:Y XO2@cwl6{se)7iLi91ߺh屭se%J } 0w]4Y0ǯ:xr>I=1>K|: /Zߗs>{ #ؙ<S^j eq tqc0+1$GX7s9R^&~0YeU$t'kw\30mH#R\ T@¤sy]VXF \)75&LY]8TqN NYa "s}j`tNkkQ#¤X%$x9xQX+תeʿCk<^)79:IT̕l~.`G X`ī:I< xƗ@~6#1S~xRP=AR:P౞s,Gj>{Y@v=胱`|` >j}Q0G x@#ЫҥXG=. "cxAtƱ=[R:;}t\1El5x"Х/)%$еS-G6%c_)БctO#`m{B7c Y:RGE&fq X5 @ܯx,tB`B.VElx<.#>VC&,dj;2n6=9ן@#WRՠsޏjU; c[TP0O]4r&t'+J^DI㦾?&&\XB9q_ sU>K]0])CKg>oWƌx/_sxD7XM T9@0l/ j52g{5NJ5|]x]=P ߳<%\(m\}AxNy+"# ,Z`<'Gkqb+ sޯ{ vSG̛ǹP$LJ XV \ƄIZv:i<!$O J "\Q*ayOR 0rMEuW/)v6^Х^O:\6x;WJ 0=+ @kG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sG9#МhN4'x< @sGV٤9罈؉=]u) ]@r[uY>['SJ׺`Zff_8\Bƃy5"JyRԽs%#b[èԹ|>y{ƭIs>{ɸҩ.`u%˜rSc$ܕ'-hRc FäIqJd %?j:H 0>o ٍ*e̡ xqrT} ڶ`ī:IFd#*xn.`Q=D#:`^p`ZꙎv)# 0,U<:he.d0@+&<<ÿ?Kݺ;gN4vZ` u =L±3 lE ⱊ v0bu} {[1o5bej$`,d G[.kF.b׷m[c=wDv}9`\@W4[x4p]2H>:B<:҇ 8K]Se uˣn 0Gk57xd`<>Kc@0Yz]`̓GEZ`%xXc@/T/*FX/n>#lh$x#МhNn m. KxܧQP_uЃ0 ^\ky5M>{F@:#kZ`4vR ƾ)%/@,z$5[\LgRr&xwbxsF.&n t=#xaB( X1{htD`BV."N42{7~zȜslW+Rw4.8#_X?SJG_X8`1#Vc`E xX/úC- 0U"IDAT:x3#f/9筈(!{}<^$s/|eajSO3tqEse)T*TSƄO)=#O;i  ,``<^UV?!|{+[aFyV?nk8:tLȼ9x9DQD2rW^8aK:E0rKs~0Z_jxoZZX.s] tS璧)K] 0n͂r{-%(eJBЅWCs]?u>i TDcn@ϳIENDB`robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/icons/000077500000000000000000000000001477533125200313105ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/icons/github-icon.svg000066400000000000000000000016321477533125200342430ustar00rootroot00000000000000github-iconrobotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/icons/icon-search.svg000066400000000000000000000006751477533125200342340ustar00rootroot00000000000000Asset 4robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/icons/nav.svg000066400000000000000000000006611477533125200326200ustar00rootroot00000000000000 nav robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/js/000077500000000000000000000000001477533125200306115ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/js/fontparts_theme.js000066400000000000000000000014001477533125200343440ustar00rootroot00000000000000function collapseSidebar() { var sidebar = document.querySelector('#sidebar'); if (sidebar != null) { sidebar.classList.add('mobile-slideout') } }; window.addEventListener('DOMContentLoaded', function () { collapseSidebar() var navButton = document.querySelector('#nav-button'); navButton.addEventListener('click', toggleNav) }, true); function toggleNav() { var fpNavSidebar = document.querySelector('#sidebar'); fpNavSidebar.classList.toggle('expanded') var fpNavOverlay = document.querySelector('#mobile-nav-overlay'); fpNavOverlay.classList.toggle('hidden') fpNavOverlay.addEventListener('click', toggleNav) var fpNavIcon = document.querySelector('#nav-icon'); fpNavIcon.classList.toggle('open') }robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/js/readthedocs.js000066400000000000000000000156301477533125200334410ustar00rootroot00000000000000/* sphinx_rtd_theme version 0.4.1 | MIT license */ /* Built 20180727 10:07 */ require = function n(e, i, t) { function o(s, a) { if (!i[s]) { if (!e[s]) { var l = "function" == typeof require && require; if (!a && l) return l(s, !0); if (r) return r(s, !0); var c = new Error("Cannot find module '" + s + "'"); throw c.code = "MODULE_NOT_FOUND", c } var u = i[s] = { exports: {} }; e[s][0].call(u.exports, function (n) { var i = e[s][1][n]; return o(i || n) }, u, u.exports, n, e, i, t) } return i[s].exports } for (var r = "function" == typeof require && require, s = 0; s < t.length; s++) o(t[s]); return o }({ "sphinx-rtd-theme": [function (n, e, i) { var jQuery = "undefined" != typeof window ? window.jQuery : n("jquery"); e.exports.ThemeNav = { navBar: null, win: null, winScroll: !1, winResize: !1, linkScroll: !1, winPosition: 0, winHeight: null, docHeight: null, isRunning: !1, enable: function (n) { var e = this; void 0 === n && (n = !0), e.isRunning || (e.isRunning = !0, jQuery(function (i) { e.init(i), e.reset(), e.win.on("hashchange", e.reset), n && e.win.on("scroll", function () { e.linkScroll || e.winScroll || (e.winScroll = !0, requestAnimationFrame(function () { e.onScroll() })) }), e.win.on("resize", function () { e.winResize || (e.winResize = !0, requestAnimationFrame(function () { e.onResize() })) }), e.onResize() })) }, enableSticky: function () { this.enable(!0) }, init: function (n) { n(document); var e = this; this.navBar = n("div.wy-side-scroll:first"), this.win = n(window), n(document).on("click", "[data-toggle='wy-nav-top']", function () { n("[data-toggle='wy-nav-shift']").toggleClass("shift"), n("[data-toggle='rst-versions']").toggleClass("shift") }).on("click", ".wy-menu-vertical .current ul li a", function () { var i = n(this); n("[data-toggle='wy-nav-shift']").removeClass("shift"), n("[data-toggle='rst-versions']").toggleClass("shift"), e.toggleCurrent(i), e.hashChange() }).on("click", "[data-toggle='rst-current-version']", function () { n("[data-toggle='rst-versions']").toggleClass("shift-up") }), n("table.docutils:not(.field-list,.footnote,.citation)").wrap("
"), n("table.docutils.footnote").wrap("
"), n("table.docutils.citation").wrap("
"), n(".wy-menu-vertical ul").not(".simple").siblings("a").each(function () { var i = n(this); expand = n(''), expand.on("click", function (n) { return e.toggleCurrent(i), n.stopPropagation(), !1 }), i.prepend(expand) }) }, reset: function () { var n = encodeURI(window.location.hash) || "#"; try { var e = $(".wy-menu-vertical"), i = e.find('[href="' + n + '"]'); if (0 === i.length) { var t = $('.document [id="' + n.substring(1) + '"]').closest("div.section"); 0 === (i = e.find('[href="#' + t.attr("id") + '"]')).length && (i = e.find('[href="#"]')) } i.length > 0 && ($(".wy-menu-vertical .current").removeClass("current"), i.addClass("current"), i.closest("li.toctree-l1").addClass("current"), i.closest("li.toctree-l1").parent().addClass("current"), i.closest("li.toctree-l1").addClass("current"), i.closest("li.toctree-l2").addClass("current"), i.closest("li.toctree-l3").addClass("current"), i.closest("li.toctree-l4").addClass("current")) } catch (o) { console.log("Error expanding nav for anchor", o) } }, onScroll: function () { this.winScroll = !1; var n = this.win.scrollTop(), e = n + this.winHeight, i = this.navBar.scrollTop() + (n - this.winPosition); n < 0 || e > this.docHeight || (this.navBar.scrollTop(i), this.winPosition = n) }, onResize: function () { this.winResize = !1, this.winHeight = this.win.height(), this.docHeight = $(document).height() }, hashChange: function () { this.linkScroll = !0, this.win.one("hashchange", function () { this.linkScroll = !1 }) }, toggleCurrent: function (n) { var e = n.closest("li"); e.siblings("li.current").removeClass("current"), e.siblings().find("li.current").removeClass("current"), e.find("> ul li.current").removeClass("current"), e.toggleClass("current") } }, "undefined" != typeof window && (window.SphinxRtdTheme = { Navigation: e.exports.ThemeNav, StickyNav: e.exports.ThemeNav }), function () { for (var n = 0, e = ["ms", "moz", "webkit", "o"], i = 0; i < e.length && !window.requestAnimationFrame; ++i) window.requestAnimationFrame = window[e[i] + "RequestAnimationFrame"], window.cancelAnimationFrame = window[e[i] + "CancelAnimationFrame"] || window[e[i] + "CancelRequestAnimationFrame"]; window.requestAnimationFrame || (window.requestAnimationFrame = function (e, i) { var t = (new Date).getTime(), o = Math.max(0, 16 - (t - n)), r = window.setTimeout(function () { e(t + o) }, o); return n = t + o, r }), window.cancelAnimationFrame || (window.cancelAnimationFrame = function (n) { clearTimeout(n) }) }() }, { jquery: "jquery" }] }, {}, ["sphinx-rtd-theme"]);robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass2css.py000066400000000000000000000025221477533125200323140ustar00rootroot00000000000000# compile sass into css import os import os.path import subprocess # sass (CSS extension language) compiler # http://sass-lang.com/install # install postCSS and autoprixer with: ## npm install -g postcss-cli-simple ## npm install -g autoprefixer def compileSass(sassPath): ''' Compile a sass file (and dependencies) into a single css file. ''' cssPath = os.path.splitext(sassPath)[0] + ".css" # subprocess.call(["sass", sassPath, cssPath]) print("Compiling Sass") process = subprocess.Popen(["sass", sassPath, cssPath]) process.wait() def autoprefixCSS(sassPath): ''' Take CSS file and automatically add browser prefixes with postCSS autoprefixer ''' print("Autoprefixing CSS") cssPath = os.path.splitext(sassPath)[0] + ".css" command = "postcss --use autoprefixer --autoprefixer.browsers '> 5%' -o" + cssPath + " " + cssPath subprocess.call(command, shell=True) # gets path for directory of sass2css.py baseFolder = os.path.split(os.path.abspath(__file__))[0] for f in os.listdir(baseFolder): name, extension = os.path.splitext(f) # note: be sure that you import files from /partials into the the main.sass file, or code won't make it into CSS if extension == ".sass": sassPath = os.path.join(baseFolder, f) compileSass(sassPath) autoprefixCSS(sassPath) robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials/000077500000000000000000000000001477533125200330455ustar00rootroot00000000000000animations.sass000066400000000000000000000002131477533125200360170ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials.fade-in opacity: 0 animation: fadeIn 2s .5s ease forwards @keyframes fadeIn from opacity: 0 to opacity: 1content.sass000066400000000000000000000071141477533125200353360ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials#content vertical-align: top margin-bottom: $padding display: inline-block padding:0 1rem @media screen and (max-width: $tablet-portrait) width: 100vw order: -1 h1 + .section h2:first-of-type padding-top: 0 p max-width: $max-text-width ul li margin-bottom: 1em max-width: $max-text-width word-wrap: break-word // text-indent: -0.5em dl ul li, ul.simple li margin-bottom: 0 ul li:before content: "–" position: absolute transform: translateX(-1rem) .toctree-wrapper ul li margin-top: 0em margin-bottom: 0em dt code, dt span display: inline-block @media screen and (max-width: $tablet-portrait) word-break: break-word #fp-object-tree @media screen and (min-width: $computer-average) transform: scale(1.05) margin-left: -2.5% margin-right: -2.5% #pagination display: grid grid-template-columns: 1fr 1fr grid-gap: $padding margin-top: 4*$padding margin-bottom: 6*$padding @media screen and (min-width: 1200px) margin-left: -$padding margin-right: -$padding //////// TODO: if only 1 link, make full-width @media screen and (max-width: $tablet-portrait) word-break: break-word grid-template-columns: 1fr margin-left: $padding margin-right: $padding a display: block background: $offwhite-gray border: $focus-border border-color: $offwhite-gray border-radius: 1em padding: $padding padding-bottom: 2 * $padding transition: $short-transition &:focus outline: none border: $focus-border p:after font-family: $monofont display: block color: rgba(0,0,0,0) transform: translateY(0.4em) &#prev-link p:last-child:after content: "<<<<" &#next-link p:last-child:after content: ">>>>" &:hover border-color: $blue &:hover, &:focus background: $blue color: #fff p:first-child color: white opacity: 0.5 p:after color: rgba(0,0,0,0.2) transition: $short-transition &#prev-link p:last-child:after animation: pulse-left 1s linear infinite &#next-link p:last-child:after animation: pulse-right 1s linear infinite p margin: 0 font-family: $sansfont &:first-child color: $dark-navy text-transform: uppercase transition: $short-transition &:last-child font-size: 1.5em font-weight: bold &>#next-link text-align: right @keyframes pulse-right 0% content: ">>>>>\0000a0" 15% content: ">>>>\0000a0>" 30% content: ">>>\0000a0>>" 45% content: ">>\0000a0>>>" 60% content: ">\0000a0>>>>" 75% content: "\0000a0>>>>>" 100% content: "\0000a0>>>>>" @keyframes pulse-left 0% content: "\0000a0<<<<<" 15% content: "<\0000a0<<<<" 30% content: "<<\0000a0<<<" 45% content: "<<<\0000a0<<" 60% content: "<<<<\0000a0<" 75% content: "<<<<<\0000a0" 100% content: "<<<<<\0000a0"elements.sass000066400000000000000000000064721477533125200355060ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials//////////////////////////////////////////////////////// //////////// basic html elements and styles //////////// //////////////////////////////////////////////////////// ::selection background: $green-highlight // rgba($greener, 0.35) body margin: 0 font-family: $sansfont font-size: 1.15em line-height: 1.6em position: relative min-height: 100vh pre, code, tt font-family: $monofont font-size: 0.95em font-style: normal font-weight: normal white-space: pre-wrap pre, code, tt, .method dt @media (max-width: 600px) font-size: 1rem pre background: $offwhite-gray border: $lighter-gray solid 1px // span.pre, dt code dt code, p code background: $offwhite-gray border: none border-radius: .25em padding: 0.125em margin-left: -0.125em margin-right: -0.125em dt, dt code, dt span, dt em font-family: $monofont font-weight: bold font-style: normal dt a, dt a span font-family: $sansfont font-weight: normal table span.pre background: none border: none a, a:active text-decoration: none font-style: normal color: $blue transition: .25s &:hover color: $green &.underline border-bottom: 1px solid white &:hover border-bottom: 1px solid $green &:focus outline: 2px solid $green outline-offset: 4px button, a.button border: none display: inline-block font-family: $monofont font-size: $font-smallish background: $blue border-radius: 12px padding: 8px 16px color: white transition: .25s &:hover background: $green color: white cursor: pointer &:focus outline: 2px solid $green outline-offset: 4px hr color: none background-color: none border: none border-top: $border height: 1px margin: 2em 0 dl margin: 0 padding-bottom: $padding dt padding-top:$header-height .rst-badge dl, .rst-badge dt padding-top: 0 margin: 0 font-family: $sansfont dd @media screen and (max-width: $tablet-portrait) margin-left: 0 padding-left: 0 h1, h2, h3 font-family: $sansfont color: black line-height: 1.2em padding-top: $header-height //helps position headline visibly when it is directly linked to margin-top: 0 margin-bottom: 1rem h1 font-size: 2.25em h2 font-size: 1.75em ol margin-left: -1em max-width: 100% ul // padding-left: 0 // margin-left: 1em // text-indent: -1em ul list-style: none ul.simple // text-indent: -0.5em // 0em code padding: 0 0.05em white-space: pre-line pre padding: 1em white-space: pre-wrap img width: 100% table border-collapse: collapse border: none overflow-x: auto display: block max-width: 90vw // border: solid $lighter-gray 1px tr border: none th, td padding: 0.5em border: solid 1px #DDD th background-color: #DDD a.headerlink, a.headerlink:visited visibility: hidden .note padding: $padding border-radius: $rounded-corner-1 background-color: rgba($blue, 0.1) border: rgba($blue, 0.5) solid 1px @media (min-width: $tablet-portrait) margin-left: -$padding p margin: 0 .admonition-title // font-weight: bold color: $blue margin-bottom: 0 .highlight background: transparent !important max-width: 100vw @media (max-width: $tablet-portrait) // margin-left: -$padding pre border-radius: $rounded-corner-1 overflow: auto h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink visibility: visiblegeneral.sass000066400000000000000000000072651477533125200353100ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials/////////////////////////////////////////////////////////// //////////// general layout and overall styles //////////// /////////////////////////////////////////////////////////// .central-column-wrapper width: 100% max-width: $central-column-wrapper margin-left: auto margin-right: auto .central-column-wrapper-wide @extend .central-column-wrapper max-width: $central-column-wrapper-wide .columns-2 display: grid grid-template-columns: auto auto grid-gap: $padding @media (max-width: $tablet-portrait) grid-template-columns: auto .hidden--small-screen @media (max-width: $tablet-landscape) display: none !important .hidden--non-small-screen @media (min-width: $tablet-landscape + 1) display: none !important #wrapper min-height: calc(100vh - #{$header-height}) display: grid grid-template-columns: 260px 1fr // grid-template-areas: ". content" position: relative // margin: $padding // padding: calc(5vw + #{$header-height}) $padding 5vw // display: block .document padding-top: $header-height min-height: 100vh #intro padding: 2em $padding // text-align: center border-bottom: $border background: $dark-navy width: 100% min-height: calc(90vh - #{$header-height}) margin: auto display: flex align-items: center img display: block width: 100% margin-left: auto margin-right: auto margin-bottom: 10vh transition: .25s ease max-width: 100vw @media (min-width: $tablet-landscape) transition: .25s ease max-width: 90vw p margin:0 margin-bottom:$padding max-width: 55ch color: $lighter-gray a color: $lighter-gray border-bottom: 1px solid $lighter-gray &:hover color: $green border-bottom: 1px solid $green ul margin:0 display: flex justify-content: space-between justify-items: flex-end flex-flow: row wrap max-width: 400px li display: inline-block list-style: none margin-bottom:$padding .columns-2 grid-template-columns: 2fr 1fr margin-left: auto margin-right: auto .button float: right @media (max-width: $tablet-landscape) grid-template-columns: auto .button float: left .button #download // margin-right: 2em .toctree-wrapper ul text-indent: 0 #designers, #developers display: inline-block vertical-align: top width: 48% #developers float: right #footer @extend #header height: $footer-height position: absolute z-index: 1 width: 100% padding: $padding*1.5 $padding &>div display: flex align-items: flex-start justify-content: space-between @media (max-width: $tablet-landscape) flex-flow: row wrap a width: 100% p margin: 0 div:first-child order: 2 @media (max-width: $tablet-landscape) order: 0 padding-bottom: 1em #home_button position: fixed bottom: 1em right: 1em z-index: 4 opacity: .15 background: black padding: 20px border-radius: 12px color: white transition: opacity .75s &:hover opacity: 1 .icon-24 // max-width: 24px // max-height: 24px width: 24px height: 24px // overriding styles in injected readthedocs versions toggle .rst-versions position: absolute !important bottom: -$footer-height !important right: $padding !important font-size: 1rem !important z-index: 1 !important width: auto border-radius: .5rem margin: 1rem 0 .rst-current-version width: auto height: 2rem border-radius: .5rem /* ---------- media queries ---------- */ @media screen and (max-width: $tablet-landscape) #wrapper // display: block grid-template-columns: 1fr #designers, #developers display: block width: 100% #content // margin-right: $contentWidth width: 100vw order: -1 #sidebar display: block width: 100% min-height: none #sidebar-inner-wrapper position: relative navbar.sass000066400000000000000000000033621477533125200351360ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials#header height: $header-height position: fixed z-index: 2 width: 100% background: black // padding: 0 $padding * 2 font-family: $monofont font-size: 0.75em color: white display: flex align-items: center a color: white a:hover color: $green font-weight: bold .navbar display: flex width: 100% padding-left: $padding padding-right: $padding ul padding-left: 0 white-space: nowrap ul, li, #searchbox display: inline-block zoom: 1 nav display: flex align-items: center justify-content: space-between width: 100% div display: flex justify-content: space-between align-items: center a:not(:last-child) margin-right: 1rem svg width: 32px .navbar a svg path, .navbar a svg line transition: 0.25s ease .navbar a svg:hover path fill: $green !important .navbar a svg:hover line stroke: $green !important .navbar button background: none padding: 0 transition: .25s ease outline: none svg width: 32px line transition: .25s ease &:hover, &:focus svg line stroke: $green !important &#line1 transform: rotate(-45deg) scale(0.8, 1) &#line2 transform-origin: 100% 50% transform: scale(0.8, 1) &#line3 transform: rotate(45deg) scale(0.8, 1) svg.open line &#line1 transform: rotate(45deg) scale(0.8, 1) &#line2 transform-origin: 0% 50% transform: scale(0.8, 1) &#line3 transform: rotate(-45deg) scale(0.8, 1) .inline-items margin-top: 0 li border-left: $border margin-left: 0 padding-left: 10px padding-right: 10px &:first-child border: none margin-left: 0 padding-left: 0 padding-right: 10px &:last-child padding-right: 0 input border: none border-radius: .25rem height: 1.5rem padding: .25remobjects-index.sass000066400000000000000000000022311477533125200364150ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials.genindex-jumpbox position: sticky top: 3.5rem background: $offwhite-gray padding: 0 1rem margin: 1rem -1rem 0 border-radius: 1rem color: rgba($light-gray,0.5) z-index: 1 min-height: 2.5rem display: flex align-items: center justify-content: space-between flex-wrap: wrap @media (max-width: $tablet-landscape) padding: 0 2rem margin: 1rem -2rem 0 strong color: inherit a font-family: $monofont padding: 0.5em 0.25rem // color: white background: rgba($light-gray,0) transition: .25s ease &:hover color: white background: rgba($light-gray,1) #content .genindextable ul padding-left: 0.75em // &:first-child // padding-left: 0.5em ul li margin: 0 line-height: 1.25 ul li:before font-family: $monofont content: '→' color: $light-gray ul:not(:first-child) li:before content: '•' a display: inline-block margin-bottom: 0.75emreset.sass000066400000000000000000000002731477533125200350050ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials/* ---------- resets etc ---------- */ html -moz-box-sizing: border-box -webkit-box-sizing: border-box box-sizing: border-box *, *:before, *:after box-sizing: inheritsearch.sass000066400000000000000000000024361477533125200351330ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials#searchbox width: 60vw #searchbox form font-family: $monofont display: flex height: 1.75rem padding-left: 1.75rem margin-right: 1rem justify-content: flex-end #searchbox img display: block width: 16px height: 16px position: relative top: 6px left: 24px #searchbox input[type="text"] border-radius: 0.25rem font-family: $monofont font-size: 1em background: transparent border: #444 1px solid color: white transition: 0.25s ease height: 100% padding-left: 1.75rem width: 60% min-width: 100px max-width: 200px &:hover border: $green 1px solid &:focus outline: none background: #444 border: #444 1px solid // border: none // white 1px solid width: 100% max-width: 400px + button background: $blue border: $blue 1px solid #searchbox p display: none .highlighted background: $green-highlight border: $green 1px solid border-radius: $rounded-corner-2 font-style: normal #content #search-results ul padding-left: 0 li margin-bottom: $padding*2 &:before content: none a font-family: $sansfont font-size: 1.5remsidebar.sass000066400000000000000000000043661477533125200353030ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials#sidebar position: sticky top: $header-height max-height: calc(100vh - #{$header-height}) max-width: $sidebarWidth z-index: 1 padding: 0 0 0 1rem font-size: 1rem font-family: $sansfont background: white display: flex ul margin: 0 margin-bottom: $padding li padding: 0 h3 margin-bottom: 0.5em font-size: 1rem font-weight: bold .caption margin: 0 text-transform: uppercase font-size: 80% letter-spacing: 0.1em @media (max-width: $tablet-landscape) z-index: 2 &.mobile-slideout position: fixed width: $sidebarWidth max-width: 100vh height: calc(100vh - #{$header-height}) left: 100vw top: $header-height transition: 0.5s ease border-right: none padding: 0 overflow-y: auto opacity: 0 .sidebar-inner-wrapper padding: 0 1rem 2rem 1rem transform: none overflow-y: show &.expanded opacity: 1 transform: translateX(-100%) transition: 0.5s ease // the below wrapper div could probably be refactored to be eliminated – the stickiness now happens on the parent .sidebar-inner-wrapper width: 100% padding: 3rem 1rem 4rem 1rem //3rem 1rem overflow-y: auto transform: translateX(-1rem) border-right: solid #dddddd 1px @media screen and (max-width: $tablet-landscape) max-height: none padding-left: 2em a display: block margin-left: -0.5em padding: 0.25em 0.5em border-radius: 0.5em background: white border: white solid .125em transition: background 1s ease-out line-height: 1 color: black display: grid grid-template-columns: auto 1fr align-items: center span padding-left: .5em a:focus outline: 0 border: $focus-border // box-shadow: 0px 0px .75em $greener a:hover color: $blue background: $offwhite-gray// rgba($greener, 0.35) transition: background .125s ease a.current background: $blue //$offwhite-gray color: white &>ul padding: 0 ul padding-left: 1em &>li>a color: black font-weight: bold // &>ul ul // height: 0px // overflow: hidden #mobile-nav-overlay position: fixed width: 100vw height: 100vh background: rgba($dark-navy, 0.75) opacity: 1 pointer-events: all transition: .25s ease cursor: e-resize z-index: 2 top: $header-height &.hidden opacity: 0 pointer-events: none syntax-highlighting.sass000066400000000000000000000004261477533125200376540ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partialsbody .highlight .k color: $blue .gp color: $red .s2 color: $mid-blue .ow, .nb color: $mid-green .bp color: $purple .c1 // comments font-style: normalvariables.sass000066400000000000000000000026111477533125200356310ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/static/sass_partials///////////// fonts ///////////// $monofont: 'Source Code Pro', 'Menlo', 'Andale Mono', 'Monaco', 'Consolas', 'Courier' $sansfont: 'Source Sans Pro', Lucida Grande, Geneva, Arial, Verdana, sans-serif $italicfont: Georgia, serif ///////////// type scale ////////////// $font-small: 0.75em $font-smallish: 0.875em $font-normal: 1em $max-text-width: 70ch ///////////// sizing, etc ///////////// $padding: 1rem $border: 3px solid black $rounded-corner-1: 0.5rem $rounded-corner-2: 0.25rem $width-flexing-margin: calc(4rem + 2.5vw) $header-height: 3rem $sidebar-sticky-height: $header-height + 1rem $footer-height: 16rem $sidebarWidth: 18.75rem // remove? $contentWidth: $sidebarWidth + $padding // remove? $central-column-wrapper: 50rem // 50rem is 800px $central-column-wrapper-wide: $central-column-wrapper * 2 ///////////// colors ///////////// $green: #66DD00 $greener: #16ff00 $blue: #0062FF $dark-navy: #081b3c $light-gray: #888888 $lighter-gray: #dddddd $offwhite-gray: whitesmoke // extra syntax highlights $red: #ff5329 $mid-blue: #267ed7 $mid-green: #008e20 $purple: #8800ff $green-highlight: rgba($greener, 0.35) ///////////// repeating styles ////////// $focus-border: $green solid .125em $short-transition: .25s ease // ////////// breakpoints ///////// $computer-average: 1200px $tablet-landscape: 1000px $tablet-portrait: 800pxrobotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/theme.conf000066400000000000000000000006171477533125200306630ustar00rootroot00000000000000[theme] inherit = basic stylesheet = fontparts.css [options] rightsidebar = false stickysidebar = false collapsiblesidebar = false externalrefs = true bodyfont = "Lucida Grande", Monospace headerfont = Menlo, Monospace bgcolor = #FFFFFF textcolor = #3B3A3A linkcolor = #FF0000 headercolor1 = #00FF00 headercolor2 = #0000FF headerlinkcolor = #F2462C codebgcolor = #FFFFFF codetextcolor = #3B3A3A robotools-fontParts-26e8b8c/documentation/source/_themes/fontPartsTheme/versions.html000066400000000000000000000024341477533125200314470ustar00rootroot00000000000000 {% if READTHEDOCS %} {# Add rst-badge after rst-versions for small badge style. #}
Read the Docs v: {{ current_version }}
{{ _('Versions') }}
{% for slug, url in versions %}
{{ slug }}
{% endfor %}
{{ _('Downloads') }}
{% for type, url in downloads %}
{{ type }}
{% endfor %}
{{ _('On Read the Docs') }}
{{ _('Project Home') }}
{{ _('Builds') }}

{% trans %}Free document hosting provided by Read the Docs.{% endtrans %}
{% endif %}robotools-fontParts-26e8b8c/documentation/source/conf.py000066400000000000000000000332571477533125200236300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # FontParts documentation build configuration file, created by # sphinx-quickstart on Thu Mar 24 13:04:20 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os sys.path.insert(0, os.path.abspath('../../Lib')) # ------------ # Mock Imports # This try/except is a hack around an issue in Mac OS 10.11.5. # Specifically, mock requires a version of six that is later # than the one that comes installed with the OS. This hack # tries to import defcon and if it can't, it kicks to mock. # This makes both local and readthedocs compilation work. try: import defcon except ImportError: from mock import Mock as MagicMock class Mock(MagicMock): @classmethod def __getattr__(cls, name): return Mock() MOCK_MODULES = [ 'fontTools', 'fontTools.misc', 'fontTools.misc.py23', 'fontTools.pens', 'fontTools.pens.basePen', 'fontMath', 'ufoLib', 'ufoLib.pointPen', 'defcon' ] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) # / Mock Imports # -------------- # ------------ # Monkey Patch # # https://github.com/sphinx-doc/sphinx/issues/1254 # from fontParts.base.base import dynamicProperty dynamicProperty.__get__ = lambda self, *args, **kwargs: self # # /MonkeyPatch # ------------ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary', ] autodoc_member_order = 'bysource' autoclass_content = 'both' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'contents' # was 'index' # General information about the project. project = u'FontParts' copyright = u'2016, Dr. Rob O. Fab' author = u'Dr. Rob O. Fab' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'0.1' # The full version, including alpha/beta/rc tags. release = u'0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = False # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'fontPartsTheme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'FontPartsdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'FontParts.tex', u'FontParts Documentation', u'Dr. Rob O. Fab', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'fontparts', u'FontParts Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'FontParts', u'FontParts Documentation', author, 'FontParts', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False #################### # custom directives from docutils import nodes from docutils.statemachine import ViewList from sphinx import addnodes from sphinx.util import rst from docutils.parsers.rst import directives from sphinx.ext.autosummary import Autosummary, import_by_name, get_import_prefixes_from_env, autosummary_table class AutosummaryMethodList(Autosummary): option_spec = dict(Autosummary.option_spec) option_spec["hidesummary"] = directives.flag def get_items(self, names): """ Subclass get items to get support for all methods in an given object """ env = self.state.document.settings.env prefixes = get_import_prefixes_from_env(env) methodNames = [] for name in names: methodNames.append(name) _, obj, _, _ = import_by_name(name, prefixes=prefixes) methodNames.extend(["%s.%s" % (name, method) for method in dir(obj) if not method.startswith("_")]) return super(AutosummaryMethodList, self).get_items(methodNames) def get_table(self, items): """ Subclass to get support for `hidesummary` as options to enable displaying the short summary in the table """ hidesummary = 'hidesummary' in self.options table_spec = addnodes.tabular_col_spec() table_spec['spec'] = 'p{0.5\linewidth}p{0.5\linewidth}' table = autosummary_table('') real_table = nodes.table('', classes=['longtable']) table.append(real_table) group = nodes.tgroup('', cols=2) real_table.append(group) group.append(nodes.colspec('', colwidth=10)) group.append(nodes.colspec('', colwidth=90)) body = nodes.tbody('') group.append(body) def append_row(*column_texts): row = nodes.row('') for text in column_texts: node = nodes.paragraph('') vl = ViewList() vl.append(text, '') self.state.nested_parse(vl, 0, node) try: if isinstance(node[0], nodes.paragraph): node = node[0] except IndexError: pass row.append(nodes.entry('', node)) body.append(row) for name, sig, summary, real_name in items: qualifier = 'obj' if 'nosignatures' not in self.options: col1 = ':%s:`%s <%s>`\ %s' % (qualifier, name, real_name, rst.escape(sig)) else: col1 = ':%s:`%s <%s>`' % (qualifier, name, real_name) col2 = summary if hidesummary: append_row(col1) else: append_row(col1, col2) return [table_spec, table] def setup(app): app.add_directive('autosummarymethodlist', AutosummaryMethodList) from pygments.style import Style from pygments.token import Keyword, Name, Comment, String, Error, \ Number, Operator, Generic class YourStyle(Style): default_style = "" styles = { Comment: 'italic #888', Keyword: 'bold #005', Name: '#f00', Name.Function: '#0f0', Name.Class: 'bold #0f0', String: 'bg:#eee #111' }robotools-fontParts-26e8b8c/documentation/source/contents.rst000066400000000000000000000003751477533125200247130ustar00rootroot00000000000000Designers ========= .. toctree:: :maxdepth: 2 :caption: Type Designers gettingstarted/index objectref/index Developers ========== .. toctree:: :maxdepth: 1 :caption: Software Developers environments/index development/indexrobotools-fontParts-26e8b8c/documentation/source/development/000077500000000000000000000000001477533125200246415ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/development/documenting.rst000066400000000000000000000167601477533125200277210ustar00rootroot00000000000000.. highlight:: python ########### Documenting ########### In general, follow the suggestions `here `_. ************** Tones of Voice ************** The documentation needs to speak to three different groups of people, each with different needs. #. Designers writing scripts that use fontParts. This is the *Public API*. #. Developers implementing fontParts inside their own application. This is the *Implementation API*. #. Developers working on fontParts itself. This is the *Internal Stuff*. Each of these needs a different tone of voice, level of detail and so on. Public API ========== Most people reading the Public API will be looking for specific information about a specific method, property, etc. The text needs to be brief, easy to understand and quick to get the gist of. * write as if talking * don't assume that the reader has more than basic Python proficiency * be detailed, but not overwhelming * keep technical jargon to a minimum * show examples Examples -------- Examples should be simple and concise. They should show what is being documented and nothing else. The point of the documentation examples is to show how to use the method/function being explained, not to make a fully functional script. (Fully functional demo scripts will be a separate type of documentation.) * use interactive prompt style with ``>>>`` * use Python 3 syntax * don't show more functionality than is necessary for what the documentation is explaining * don't show ``import`` statements unless necessary * don't show constructors unless that is what is being demonstrated * multiple examples each showing one thing are better than one example showing multiple things Do this: :: >>> font.glyphOrder = ["A", "B", "C"] >>> font.glyphOrder ["A", "B", "C"] Don't do this: :: import random from fontParts.world import * font = CurrentFont() print(font.glyphOrder) font.glyphOrder = sorted(font.glyphOrder) print(font.glyphOrder) order = list(font.glyphOrder) random.shuffle(order) font.glyphOrder = order print(font.glyphOrder) Implementation API ================== Developers reading the Implementation API will be looking for detailed information about what they need to do. The text needs to make their work as easy as possible. * write concisely * assume that the reader has computer science proficiency * cover requirements thoroughly * explain what can be expected of incoming data * explain what is expected of outgoing data * don't leave anything to assumption or interpretation * always use may/must/should terminology Internal Stuff ============== The code within fontParts itself should be written such that what the code is doing is immediately apparent. Thus, it should require very limited documentation, if any at all. * assume that the reader knows how fontParts works * funny jokes are allowed, but only if they are funny ********************* Documentation Strings ********************* Most of the documentation will be contained with the source code itself. Here's the structure of how it should be done: :: class BaseThing(BaseOtherThing): """ This is a very brief explanation of the object. A note about when to create this object may be added. This text will be prepended to the string in __init__ in the compiled documentation. """ def aMethod(arg, kwarg="blah"): """ A very brief description calling out majorly significant ``args``. >>> blah.public() "output" The next level of documentation is presented in paragraph form. This will detail what ``arg`` means/does, it's potential options (linking to :ref:`type-detail` or :class:`ObjectClass` as needed, the default value, any possible errors and so on. If a list is needed to detail what the method does, it should be presented as a list: * this happens * that happens * finally this happens It should read very simply and clearly. Next is a description of ``kwarg`` following the same form. If an argument has options they are to be presented as a table. +---------+-----------------------+ | option1 | Sentence description. | +---------+-----------------------+ | option2 | Sentence description. | +---------+-----------------------+ Further explanations carry on for additional arguments and so on. .. note:: If there is a special note, put it in a note section. """ def _aMethod(arg, kwarg="blah"): """ This is the environment implementation of :meth:`BaseThing.aMethod`. ``arg`` will be a :ref:`type-detail` that has been normalized with :func:`normalizers.normalizeValue`. If there are any notes on how to interpret this, it goes here. ``kwarg`` is now explained. The options for kwarg are detailed in :meth:`BaseThing.aMethod` rather than duplicated here. If something goes wrong a :exc:`FontPartsError` (or other applicable) error must be raised. This method must return a result of :ref:`type-detail` and the returned value will be normalized with :func:`normalizers.normalizeValue`. Subclassing statement such as: Subclasses may override this method. """ aProperty = dynamicProperty( "base_aProperty", """ A very brief description with optional :ref:`type-detail`. >>> print(font.aProperty) "output" Additional info if needed. """ ) *************** Quick Reference *************** Basic Formatting ================ :: *emphasis (italics)* **strong (bold)** ``code`` Always use this for things like args, kwargs, ``True``, ``False`` and ``None``. `Some text `_ :mod:`module` :func:`module.functionName` :class:`ClassName` :meth:`ClassName.methodName` :attr:`ClassName.attribute` :exc:`ExceptionName` :ref:`my-reference-label` * unordered * list #. ordered #. list +---------+--------------+ | option1 | Description. | +---------+--------------+ | option2 | Description. | +---------+--------------+ Frequently Used Stuff ===================== Statements ---------- * This attribute is read only. * Subclasses must override this method. * Subclasses may override this method. Value Types ----------- fontParts ^^^^^^^^^ * ``:ref:`type-string``` * ``:ref:`type-int-float``` * ``:ref:`type-coordinate``` * ``:ref:`type-angle``` * ``:ref:`type-identifier``` * ``:ref:`type-color``` * ``:ref:`type-transformation``` * ``:ref:`type-immutable-list``` general ^^^^^^^ * ``:ref:`type-string``` * ``:ref:`type-int``` * ``:ref:`type-float``` * ``:ref:`type-hex``` * ``:ref:`type-bool``` * ``:ref:`type-tuple``` * ``:ref:`type-list``` * ``:ref:`type-dict``` * ``:ref:`type-set``` Heading Levels ============== :: ####### Level 1 ####### ******* Level 2 ******* Level 3 ======= Level 4 ------- Level 5 ^^^^^^^ Level 6 """"""" Special Sections ================ :: .. note:: .. warning:: .. versionadded:: .. versionchanged:: .. seealso:: robotools-fontParts-26e8b8c/documentation/source/development/index.rst000066400000000000000000000056231477533125200265100ustar00rootroot00000000000000.. highlight:: python #################### Developing FontParts #################### You want to help with developing FontParts? Yay! We are mostly focused on documenting the objects and building a test suite. We'll eventually need bits of code here and there. If you have an idea for a new API or want to discuss one of the testing APIs, cool. .. _developing-proposals: ********* Proposals ********* Want to suggest a new font part for FontParts? It's best to do this as an issue on the `FontParts GitHub `_ repository. Please present why you think this needs to be added. Before you do so, please make sure you understand the goals of the project, the existing API and so on. .. _developing-bug-reports: *********** Bug Reports *********** Notice a bug when using FontParts? Is it a bug in a specific application? If so, please report the bug to the application developer. If it's not specific to a particular application, please open an issue on GitHub or, if you really can't `open an issue on GitHub `_, send a message to the `RoboFab mailing list `_ .. _developing-coding: ****** Coding ****** Take a look at the `open issues `_ and see if there is anything there that you want to work on. Please try to follow the general coding style of the library so that everything has the same level of readability. This library follows much of PEP8, with a couple of exceptions. You’ll see camelCase. We like camelCase. The standard line length is also 90 characters. If possible, try to keep lines to 80, but 90 comes in handy occasionally. You’ll also notice that some builtin names are redefined in as variables in methods. It’s impossible not to use ``type`` in a package dealing with fonts. ********************* Writing Documentation ********************* We really need help with adding the formatted documentation strings to the base objects. The API documentation is generated from those. Here's a :doc:`style guide `. Please look at the `Documentation project `_ on GitHub and see if there is anything you want to work on. If there is, ask to be assigned to that issue, and then follow the style guide. A good place to look for examples of the object documentation is the `glyph object `_. ********** Test Suite ********** We also really need help in finishing up the test suite. You can see what needs to be done in the `Tests project `_ on GitHub. Pick something you want to write tests for and ask to be assigned to that issue. More information about writing tests is :doc:`here `. robotools-fontParts-26e8b8c/documentation/source/development/testing.rst000066400000000000000000000126241477533125200270550ustar00rootroot00000000000000.. highlight:: python ####### Testing ####### The test cases are located in ``fontParts.test.test_*``. ============== Test Structure ============== :: import unittest from fontParts.base import FontPartsError class TestFoo(unittest.TestObject): # -------------- # Section Header # -------------- def getFoo_generic(self): # code for building the object def test_bar(self): # Code for testing the bar attribute. def test_changeSomething(self): # Code for testing the changeSomething method. ================ Test Definitions ================ The test definitions should be developed by following the :ref:`FontParts API documentation `. These break down into two categories. #. attributes #. methods These will be covered in detail below. In general follow these guidelines when developing #. Keep the test focused on what is relevant to what is being tested. Don't test file saving within an attribute test in a sub-sub-sub-sub object. #. Make the tests as atomic as possible. Don't modify lots of parts of an object during a single test. That makes the tests very hard to debug. #. Keep the code clear and concise so that it is easy to see what is being tested. Add documentation to clarify anything that is ambiguous. Try to imagine someone trying to debug a failure of this test five years from now. Will they be able to tell what is going on in the code? #. If testing an edge case, make notes defining where this situation is happening, why it is important and so on. Edge case tests often are hyper-specific to one version of one environment and thus have a limited lifespan. This needs to be made clear for future reference. #. Test valid and invalid input. The base implementation's normalizers define what is valid and invalid. Use this as a reference. #. Only test one thing per test case. Tests are **not** a place to avoid repeated code, it's much easier to debug an error in a test when that test is only doing one thing. Testing Attributes ------------------ Attribute testing uses the method name structure ``test_attributeName``. If more than one method is needed due to length or complexity, the additional methods use the name structure ``test_attributeNameDescriptionOfWhatThisTests``. :: def test_bar_get(self): foo, unrequested = self.getFoo_generic() # get self.assertEqual( foo.bar, "barbarbar" ) def test_bar_set_valid(self): foo, unrequested = self.getFoo_generic() # set: valid data foo.bar = "heyheyhey" self.assertEqual( foo.bar, "heyheyhey" ) def test_bar_set_invalid(self): foo, unrequested = self.getFoo_generic() # set: invalid data with self.assertRaises(FontPartsError): foo.bar = 123 def test_barSettingNoneShouldFail(self): foo, unrequested = self.getFoo_barNontShouldFail() with self.assertRaises(FontPartsError): foo.bar = None Getting ^^^^^^^ When testing getting an attribute, test the following: * All valid return data types. Use the case definitions to specify these. * (How should invalid types be handled? Is that completely the responsibility of the environment?) Setting ^^^^^^^ When testing setting an attribute, test the following: * All valid input data types. For example if setting accepts a number, test int and float. If pos/neg values are allowed, test both. * A representative sample of invalid data types/values. If an attribute does not support setting, it should be tested to make sure that an attempt to set raises the appropriate error. Testing Methods --------------- Testing methods should be done atomically, modifying a single argument at a time. For example, if a method takes x and y arguments, test each of these as independently as possible. The following should be tested for each argument: * All valid input data types. For example if setting accepts a number, test int and float. If pos/neg values are allowed, test both. * A representative sample of invalid data types/values. :: def test_changeSomething(self): bar, unrequested = self.getBar_something() bar.changeSomething(x=100, y=100) self.assertEqual( bar.thing, (100, 100) ) def test_changeSomething_invalid_x(self): bar, unrequested = self.getBar_something() with self.assertRaises(FontPartsError): bar.changeSomething(x=None, y=100) def test_changeSomething_invalid_y(self): bar, unrequested = self.getBar_something() with self.assertRaises(FontPartsError): bar.changeSomething(x=100, y=None) =================== Objects for Testing =================== Objects for testing are defined in methods with the name structure ``getFoo_description``. The base object will be generated by the environment by calling ``self.objectGenerator("classIdentifier")``. This will return a fontParts wrapped object ready for population and testing. It will also return a list of objects that were/are required for generating/retaining the requested object. For example, if an environment doesn't support orphan glyphs, the unrequested list may contain a parent font. The objects in the unrequested list must not be used within tests. :: def getFoo_generic(self): foo = self.objectGenerator("foo") foo.bar = "barbarbar" return foo, [] ===== To Do ===== - Establish tests for pen protocol in test_glyph. robotools-fontParts-26e8b8c/documentation/source/environments/000077500000000000000000000000001477533125200250465ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/environments/index.rst000066400000000000000000000014321477533125200267070ustar00rootroot00000000000000.. highlight:: python ###################### Implementing FontParts ###################### The whole point of FontParts is to present a common API to scripters. So, obviously, the way to implement it is to develop an API that is compliant with the :ref:`object documentation `. That's going to be a non-trivial amount of work, so we offer a less laborious alternative: we provide a set of :ref:`base objects ` that can be subclassed and privately mapped to an environment's native API. If you don't want to use these base objects, you can implement the API all on your own. You just have to make sure that your implementation is compatible. .. toctree:: :maxdepth: 2 :includehidden: testing/index objects/index layers/index robotools-fontParts-26e8b8c/documentation/source/environments/layers/000077500000000000000000000000001477533125200263455ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/environments/layers/index.rst000066400000000000000000000046371477533125200302200ustar00rootroot00000000000000###### Layers ###### There are two primary layer models in the font world: - font level layers: In this model, all glyphs have the same layers. A good example of this is a chromatic font. - glyph level layers: In this model, individual glyphs may have their own unique layers. fontParts supports both of these models. Both fonts and glyphs have fully developed layer APIs:: font = CurrentFont() foregroundLayer = font.getLayer("foreground") backgroundLayer = font.getLayer("background") glyph = font["A"] foregroundGlyph = glyph.getLayer("foreground") backgroundGlyph = glyph.getLayer("background") A font-level layer is a font-like object. Essentially, a layer has the same glyph management behavior as a font:: font = CurrentFont() foreground = font.getLayer("foreground") glyph = foreground.newGlyph("A") A glyph-level layer is identical to a glyph object:: font = CurrentFont() glyph = font["A"] foreground = glyph.getLayer("foreground") background = glyph.getLayer("background") When a scripter is addressing a font or glyph without specifying a specific layer, the action is performed on the "default" (or primary) layer. For example, in the original Fontographer there were two layers: foreground and background. The foreground was the primary layer and it contained the primary data that would be compiled into a font binary. In multi-layered glyph editing environments, designers can specify which layer should be considered primary. This layer is the "default" layer in fontParts. Thus:: font = CurrentFont() glyph1 = font["A"] glyph2 = font.newGlyph("B") The `glyph1` object will reference the A's "foreground" layer and the "foreground" layer will contain a new glyph named "B". fontParts delegates the implementation to the environment subclasses. Given that an environment can only support font-level layers *or* glyph-level layers, the following algorithms can be used to simulate the model that the environment doesn't support. Simulating glyph-level layers. ============================== 1. Get the parent font. 2. Iterate through all of the font's layers. 3. If the glyph's name is in the layer, grab the glyph from the layer. 4. Return all found glyphs. Simulating font-level layers. ============================= 1. Iterate over all glyphs. 2. For every layer in the glyph, create a global mapping of layer name to glyphs containing a layer with the same name. robotools-fontParts-26e8b8c/documentation/source/environments/objects/000077500000000000000000000000001477533125200264775ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/environments/objects/anchor.rst000066400000000000000000000012741477533125200305070ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Anchor ****** Must Override ------------- .. automethod:: BaseAnchor._get_color .. automethod:: BaseAnchor._get_identifier .. automethod:: BaseAnchor._get_name .. automethod:: BaseAnchor._get_x .. automethod:: BaseAnchor._get_y .. automethod:: BaseAnchor._set_color .. automethod:: BaseAnchor._set_name .. automethod:: BaseAnchor._set_x .. automethod:: BaseAnchor._set_y May Override ------------ .. automethod:: BaseAnchor._init .. automethod:: BaseAnchor._moveBy .. automethod:: BaseAnchor._rotateBy .. automethod:: BaseAnchor._scaleBy .. automethod:: BaseAnchor._skewBy .. automethod:: BaseAnchor._transformBy .. automethod:: BaseAnchor.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/objects/bpoint.rst000066400000000000000000000013131477533125200305220ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base BPoint ****** Must Override ------------- May Override ------------ .. automethod:: BaseBPoint._get_anchor .. automethod:: BaseBPoint._get_bcpIn .. automethod:: BaseBPoint._get_bcpOut .. automethod:: BaseBPoint._get_index .. automethod:: BaseBPoint._get_type .. automethod:: BaseBPoint._init .. automethod:: BaseBPoint._moveBy .. automethod:: BaseBPoint._rotateBy .. automethod:: BaseBPoint._scaleBy .. automethod:: BaseBPoint._set_anchor .. automethod:: BaseBPoint._set_bcpIn .. automethod:: BaseBPoint._set_bcpOut .. automethod:: BaseBPoint._set_type .. automethod:: BaseBPoint._skewBy .. automethod:: BaseBPoint._transformBy .. automethod:: BaseBPoint.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/objects/component.rst000066400000000000000000000021431477533125200312330ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Component ********* Must Override ------------- .. automethod:: BaseComponent._decompose .. automethod:: BaseComponent._get_baseGlyph .. automethod:: BaseComponent._get_identifier .. automethod:: BaseComponent._get_transformation .. automethod:: BaseComponent._set_baseGlyph .. automethod:: BaseComponent._set_index .. automethod:: BaseComponent._set_transformation May Override ------------ .. automethod:: BaseComponent._draw .. automethod:: BaseComponent._drawPoints .. automethod:: BaseComponent._get_bounds .. automethod:: BaseComponent._get_index .. automethod:: BaseComponent._get_offset .. automethod:: BaseComponent._get_scale .. automethod:: BaseComponent._init .. automethod:: BaseComponent._moveBy .. automethod:: BaseComponent._pointInside .. automethod:: BaseComponent._rotateBy .. automethod:: BaseComponent._round .. automethod:: BaseComponent._scaleBy .. automethod:: BaseComponent._set_offset .. automethod:: BaseComponent._set_scale .. automethod:: BaseComponent._skewBy .. automethod:: BaseComponent._transformBy .. automethod:: BaseComponent.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/objects/contour.rst000066400000000000000000000025261477533125200307270ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Contour ******* Must Override ------------- .. automethod:: BaseContour._getPoint .. automethod:: BaseContour._get_clockwise .. automethod:: BaseContour._get_identifier .. automethod:: BaseContour._insertPoint .. automethod:: BaseContour._lenPoints .. automethod:: BaseContour._removePoint .. automethod:: BaseContour._set_index May Override ------------ .. automethod:: BaseContour._appendBPoint .. automethod:: BaseContour._appendSegment .. automethod:: BaseContour._autoStartSegment .. automethod:: BaseContour._draw .. automethod:: BaseContour._drawPoints .. automethod:: BaseContour._get_bounds .. automethod:: BaseContour._get_index .. automethod:: BaseContour._get_points .. automethod:: BaseContour._get_segments .. automethod:: BaseContour._init .. automethod:: BaseContour._insertBPoint .. automethod:: BaseContour._insertSegment .. automethod:: BaseContour._len__segments .. automethod:: BaseContour._moveBy .. automethod:: BaseContour._pointInside .. automethod:: BaseContour._removeSegment .. automethod:: BaseContour._reverse .. automethod:: BaseContour._rotateBy .. automethod:: BaseContour._round .. automethod:: BaseContour._scaleBy .. automethod:: BaseContour._setStartSegment .. automethod:: BaseContour._set_clockwise .. automethod:: BaseContour._skewBy .. automethod:: BaseContour._transformByrobotools-fontParts-26e8b8c/documentation/source/environments/objects/features.rst000066400000000000000000000004221477533125200310450ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Features ******** Must Override ------------- .. automethod:: BaseFeatures._get_text .. automethod:: BaseFeatures._set_text May Override ------------ .. automethod:: BaseFeatures._init .. automethod:: BaseFeatures.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/objects/font.rst000066400000000000000000000027631477533125200302070ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Font **** Must Override ------------- .. automethod:: BaseFont._close .. automethod:: BaseFont._generate .. automethod:: BaseFont._getGuideline .. automethod:: BaseFont._get_defaultLayer .. automethod:: BaseFont._get_features .. automethod:: BaseFont._get_glyphOrder .. automethod:: BaseFont._get_groups .. automethod:: BaseFont._get_info .. automethod:: BaseFont._get_kerning .. automethod:: BaseFont._get_layerOrder .. automethod:: BaseFont._get_layers .. automethod:: BaseFont._get_lib .. automethod:: BaseFont._get_path .. automethod:: BaseFont._init .. automethod:: BaseFont._lenGuidelines .. automethod:: BaseFont._newLayer .. automethod:: BaseFont._removeGuideline .. automethod:: BaseFont._removeLayer .. automethod:: BaseFont._save .. automethod:: BaseFont._set_defaultLayer .. automethod:: BaseFont._set_glyphOrder .. automethod:: BaseFont._set_layerOrder May Override ------------ .. automethod:: BaseFont._appendGuideline .. automethod:: BaseFont._autoUnicodes .. automethod:: BaseFont._clearGuidelines .. automethod:: BaseFont._contains .. automethod:: BaseFont._getItem .. automethod:: BaseFont._getLayer .. automethod:: BaseFont._get_guidelines .. automethod:: BaseFont._insertGlyph .. automethod:: BaseFont._interpolate .. automethod:: BaseFont._isCompatible .. automethod:: BaseFont._iter .. automethod:: BaseFont._keys .. automethod:: BaseFont._len .. automethod:: BaseFont._newGlyph .. automethod:: BaseFont._removeGlyph .. automethod:: BaseFont._roundrobotools-fontParts-26e8b8c/documentation/source/environments/objects/glyph.rst000066400000000000000000000057221477533125200303620ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Glyph ***** Must Override ------------- .. automethod:: BaseGlyph._addImage .. automethod:: BaseGlyph._autoUnicodes .. automethod:: BaseGlyph._clearImage .. automethod:: BaseGlyph._getAnchor .. automethod:: BaseGlyph._getComponent .. automethod:: BaseGlyph._getContour .. automethod:: BaseGlyph._getGuideline .. automethod:: BaseGlyph._get_height .. automethod:: BaseGlyph._get_image .. automethod:: BaseGlyph._get_lib .. automethod:: BaseGlyph._get_markColor .. automethod:: BaseGlyph._get_name .. automethod:: BaseGlyph._get_note .. automethod:: BaseGlyph._get_unicodes .. automethod:: BaseGlyph._get_width .. automethod:: BaseGlyph._lenAnchors .. automethod:: BaseGlyph._lenComponents .. automethod:: BaseGlyph._lenContours .. automethod:: BaseGlyph._lenGuidelines .. automethod:: BaseGlyph._newLayer .. automethod:: BaseGlyph._removeAnchor .. automethod:: BaseGlyph._removeComponent .. automethod:: BaseGlyph._removeContour .. automethod:: BaseGlyph._removeGuideline .. automethod:: BaseGlyph._removeOverlap .. automethod:: BaseGlyph._set_height .. automethod:: BaseGlyph._set_markColor .. automethod:: BaseGlyph._set_name .. automethod:: BaseGlyph._set_note .. automethod:: BaseGlyph._set_unicodes .. automethod:: BaseGlyph._set_width May Override ------------ .. automethod:: BaseGlyph.__add__ .. automethod:: BaseGlyph.__div__ .. automethod:: BaseGlyph.__mul__ .. automethod:: BaseGlyph.__rmul__ .. automethod:: BaseGlyph.__sub__ .. automethod:: BaseGlyph._appendAnchor .. automethod:: BaseGlyph._appendComponent .. automethod:: BaseGlyph._appendContour .. automethod:: BaseGlyph._appendGlyph .. automethod:: BaseGlyph._appendGuideline .. automethod:: BaseGlyph._clear .. automethod:: BaseGlyph._clearAnchors .. automethod:: BaseGlyph._clearComponents .. automethod:: BaseGlyph._clearContours .. automethod:: BaseGlyph._clearGuidelines .. automethod:: BaseGlyph._decompose .. automethod:: BaseGlyph._getLayer .. automethod:: BaseGlyph._get_anchors .. automethod:: BaseGlyph._get_bottomMargin .. automethod:: BaseGlyph._get_bounds .. automethod:: BaseGlyph._get_components .. automethod:: BaseGlyph._get_contours .. automethod:: BaseGlyph._get_guidelines .. automethod:: BaseGlyph._get_leftMargin .. automethod:: BaseGlyph._get_rightMargin .. automethod:: BaseGlyph._get_topMargin .. automethod:: BaseGlyph._get_unicode .. automethod:: BaseGlyph._init .. automethod:: BaseGlyph._interpolate .. automethod:: BaseGlyph._isCompatible .. automethod:: BaseGlyph._iterContours .. automethod:: BaseGlyph._moveBy .. automethod:: BaseGlyph._pointInside .. automethod:: BaseGlyph._removeLayer .. automethod:: BaseGlyph._rotateBy .. automethod:: BaseGlyph._round .. automethod:: BaseGlyph._scaleBy .. automethod:: BaseGlyph._set_bottomMargin .. automethod:: BaseGlyph._set_leftMargin .. automethod:: BaseGlyph._set_rightMargin .. automethod:: BaseGlyph._set_topMargin .. automethod:: BaseGlyph._set_unicode .. automethod:: BaseGlyph._skewBy .. automethod:: BaseGlyph._transformByrobotools-fontParts-26e8b8c/documentation/source/environments/objects/groups.rst000066400000000000000000000011731477533125200305520ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Groups ****** Must Override ------------- .. automethod:: BaseGroups._contains .. automethod:: BaseGroups._delItem .. automethod:: BaseGroups._getItem .. automethod:: BaseGroups._items .. automethod:: BaseGroups._setItem May Override ------------ .. automethod:: BaseGroups._clear .. automethod:: BaseGroups._findGlyph .. automethod:: BaseGroups._get .. automethod:: BaseGroups._init .. automethod:: BaseGroups._iter .. automethod:: BaseGroups._keys .. automethod:: BaseGroups._len .. automethod:: BaseGroups._pop .. automethod:: BaseGroups._update .. automethod:: BaseGroups._valuesrobotools-fontParts-26e8b8c/documentation/source/environments/objects/guideline.rst000066400000000000000000000016221477533125200311770ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Guideline ********* Must Override ------------- .. automethod:: BaseGuideline._get_angle .. automethod:: BaseGuideline._get_color .. automethod:: BaseGuideline._get_identifier .. automethod:: BaseGuideline._get_name .. automethod:: BaseGuideline._get_x .. automethod:: BaseGuideline._get_y .. automethod:: BaseGuideline._set_angle .. automethod:: BaseGuideline._set_color .. automethod:: BaseGuideline._set_name .. automethod:: BaseGuideline._set_x .. automethod:: BaseGuideline._set_y May Override ------------ .. automethod:: BaseGuideline._get_index .. automethod:: BaseGuideline._init .. automethod:: BaseGuideline._moveBy .. automethod:: BaseGuideline._rotateBy .. automethod:: BaseGuideline._round .. automethod:: BaseGuideline._scaleBy .. automethod:: BaseGuideline._skewBy .. automethod:: BaseGuideline._transformBy .. automethod:: BaseGuideline.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/objects/image.rst000066400000000000000000000014171477533125200303160ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Image ***** Must Override ------------- .. automethod:: BaseImage._get_color .. automethod:: BaseImage._get_data .. automethod:: BaseImage._get_transformation .. automethod:: BaseImage._set_color .. automethod:: BaseImage._set_data .. automethod:: BaseImage._set_transformation May Override ------------ .. automethod:: BaseImage._get_offset .. automethod:: BaseImage._get_scale .. automethod:: BaseImage._init .. automethod:: BaseImage._moveBy .. automethod:: BaseImage._rotateBy .. automethod:: BaseImage._round .. automethod:: BaseImage._scaleBy .. automethod:: BaseImage._set_offset .. automethod:: BaseImage._set_scale .. automethod:: BaseImage._skewBy .. automethod:: BaseImage._transformBy .. automethod:: BaseImage.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/objects/index.rst000066400000000000000000000177731477533125200303570ustar00rootroot00000000000000.. highlight:: python ############################ Subclassing fontObjects.base ############################ .. _implementing-subclassing: The base objects have been designed to provide common behavior, normalization and type consistency for environments and scripters alike. Environments wrap their native objects with subclasses of fontParts' base objects and implement the necessary translation to the native API. Once this is done, the environment will inherit all of the base behavior from fontParts. Environments will need to implement their own subclasses of: .. toctree:: :maxdepth: 1 :hidden: font info groups kerning features lib layer glyph contour segment bpoint point component anchor guideline image Each of these require their own specific environment overrides, but the general structure follows this form:: from fontParts.base import BaseSomething class MySomething(BaseSomething): # Initialization. # This will be called when objects are initialized. # The behavior, args and kwargs may be designed by the # subclass to implement specific behaviors. def _init(self, myObj): self.myObj = myObj # Comparison. # The __eq__ method must be implemented by subclasses. # It must return a boolean indicating if the lower level # objects are the same object. This does not mean that two # objects that have the same content should be considered # equal. It means that the object must be the same. The # corrilary __ne__ is optional to define. # # Note that the base implentation of fontParts provides # __eq__ and __ne__ methods that test the naked objects # for equality. Depending on environmental needs this can # be overridden. def __eq__(self, other): return self.myObj == other.myObj def __ne__(self, other): return self.myObj != other.myObj # Properties. # Properties are get and set through standard method names. # Within these methods, the subclass may do whatever is # necessary to get/set the value from/to the environment. def _get_something(self): return self.myObj.getSomething() def _set_something(self, value): self.myObj.setSomething(value) # Methods. # Generally, the public methods call internal methods with # the same name, but preceded with an underscore. Subclasses # may implement the internal method. Any values passed to # the internal methods will have been normalized and will # be a standard type. def _whatever(self, value): self.myObj.doWhatever(value) # Copying. # Copying is handled in most cases by the base objects. # If subclasses have a special class that should be used # when creating a copy of an object, the class must be # defined with the copyClass attribute. If anything special # needs to be done during the copying process, the subclass # can implement the copyData method. This method will be # called automatically. The subclass must call the base class # method with super. copyClass = MyObjectWithoutUI def copyData(self, source): super(MySomething, self).copyData(source) self.myObj.internalThing = source.internalThing # Environment updating. # If the environment requires the scripter to manually # notify the environment that the object has been changed, # the subclass must implement the changed method. Please # try to avoid requiring this. def changed(self): myEnv.goUpdateYourself() # Wrapped objects. # It is very useful for scripters to have access to the # lower level, wrapped object. Subclasses implement this # with the naked method. def naked(self): return self.myObj All methods that must be overridden are labeled with "Subclasses must override this method." in the method's documentation string. If a method may optionally be overridden, the documentation string is labeled with "Subclasses may override this method." All other methods, attributes and properties **must not** be overridden. An example implementation that wraps the defcon library with fontParts is located in fontParts/objects/fontshell. Data Normalization ================== When possible, incoming and outgoing values are checked for type validity and are coerced to a common type for return. This is done with a set of functions located here: .. toctree:: :maxdepth: 1 objects/normalizers These are done in a central place rather than within the objects for consitency. There are many cases where a ``(x, y)`` tuple is defined and than rewriting all of the code to check if there are exactly two values, that each is an ``int`` or a ``float`` and so on before finally making sure that the value to be returned is a ``tuple`` not an instance of ``list``, ``OrderedDict`` or some native object we consolidate the code into a single function and call that. Environment Specific Methods, Attributes and Arguments ====================================================== FontParts is designed to be environment agnostic. Therefore, it is a 100% certainty that it doesn't do *something* that your environment does. You will want to allow your environment's *something* to be accessible through FontParts. *We* want you to allow this, too. The problem is, how do you implement *something* in a way that doesn't conflict with current or future things in FontParts? For example, let's say that you want to add a support for the "Do Something to the Font" feature you have built in your environment. You add a new method to support this:: class MyFont(BaseFont): def doSomething(self, skip=None, double=None): # go This *will* work. However, if FontParts adds a ``doSomething`` method in a later version that does something other than what or accepts different arguments than your method does, it's not going to work. Either the ``doSomething`` method will have to be changed in your implementation or you will not support the FontParts ``doSomething`` method. This is going to be lead to you being mad at FontParts, your scripters being mad at you or something else unpleasant. The solution to this problem is to prevent it from happening in the first place. To do this, environment specific methods, proprties and attributes must be prefixed with an environment specific tag followed by an ``_`` and then your method name. For example:: class MyFont(BaseFont): def myapp_doSomething(self, skip=None, double=None): # go This applies to any method, attribute or property additions to the FontParts objects. The environment tag is up to you. The only requirement is that it needs to be unique to your own environment. Method Arguments ---------------- In some cases, you are likely to discover that your environment supports specific options in a method that are not supported by the environment agnostic API. For example, your environment may have an optional heuristic that can be used in the ``font.autoUnicodes`` method. However, the ``font.autoUnicodes`` method does not have a ``useHeuristics`` argument. Unfortunately, Python doesn't offer a way to handle this in a way that is both flexible for developers and friendly for scripters. The only two options for handling this are: 1. Create an environment specific clone of the ``font.autoUnicodes`` method as ``myapp_autoUnicodes`` and add your ``useHeuristics`` argument there. 2. Contact the FontParts developers by opening a GitHub issue requesting support for your argument. If it is generic enough, we may add support for it. We're experimenting with a third way to handle this. You can see it as the ``**environmentOptions`` argument in the :meth:`BaseFont.generate` method. This may or may not move to other methods. Please contact us if you are interested in this being applied to other methods. robotools-fontParts-26e8b8c/documentation/source/environments/objects/info.rst000066400000000000000000000004761477533125200301730ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Info **** Must Override ------------- May Override ------------ .. automethod:: BaseInfo._getAttr .. automethod:: BaseInfo._init .. automethod:: BaseInfo._interpolate .. automethod:: BaseInfo._round .. automethod:: BaseInfo._setAttr .. automethod:: BaseInfo.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/objects/kerning.rst000066400000000000000000000013241477533125200306660ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Kerning ******* Must Override ------------- .. automethod:: BaseKerning._contains .. automethod:: BaseKerning._delItem .. automethod:: BaseKerning._getItem .. automethod:: BaseKerning._items .. automethod:: BaseKerning._setItem May Override ------------ .. automethod:: BaseKerning._clear .. automethod:: BaseKerning._get .. automethod:: BaseKerning._init .. automethod:: BaseKerning._interpolate .. automethod:: BaseKerning._iter .. automethod:: BaseKerning._keys .. automethod:: BaseKerning._len .. automethod:: BaseKerning._pop .. automethod:: BaseKerning._round .. automethod:: BaseKerning._scale .. automethod:: BaseKerning._update .. automethod:: BaseKerning._valuesrobotools-fontParts-26e8b8c/documentation/source/environments/objects/layer.rst000066400000000000000000000013721477533125200303500ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Layer ***** Must Override ------------- .. automethod:: BaseLayer._getItem .. automethod:: BaseLayer._get_color .. automethod:: BaseLayer._get_lib .. automethod:: BaseLayer._get_name .. automethod:: BaseLayer._keys .. automethod:: BaseLayer._newGlyph .. automethod:: BaseLayer._removeGlyph .. automethod:: BaseLayer._set_color .. automethod:: BaseLayer._set_name May Override ------------ .. automethod:: BaseLayer._autoUnicodes .. automethod:: BaseLayer._contains .. automethod:: BaseLayer._init .. automethod:: BaseLayer._insertGlyph .. automethod:: BaseLayer._interpolate .. automethod:: BaseLayer._isCompatible .. automethod:: BaseLayer._iter .. automethod:: BaseLayer._len .. automethod:: BaseLayer._roundrobotools-fontParts-26e8b8c/documentation/source/environments/objects/lib.rst000066400000000000000000000010461477533125200300000ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Lib *** Must Override ------------- .. automethod:: BaseLib._contains .. automethod:: BaseLib._delItem .. automethod:: BaseLib._getItem .. automethod:: BaseLib._items .. automethod:: BaseLib._setItem May Override ------------ .. automethod:: BaseLib._clear .. automethod:: BaseLib._get .. automethod:: BaseLib._init .. automethod:: BaseLib._iter .. automethod:: BaseLib._keys .. automethod:: BaseLib._len .. automethod:: BaseLib._pop .. automethod:: BaseLib._update .. automethod:: BaseLib._values robotools-fontParts-26e8b8c/documentation/source/environments/objects/normalizers.rst000066400000000000000000000054731477533125200316070ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base.normalizers ########### Normalizers ########### ******* Kerning ******* .. autofunction:: normalizeKerningKey .. autofunction:: normalizeKerningValue ****** Groups ****** .. autofunction:: normalizeGroupKey .. autofunction:: normalizeGroupValue ******** Features ******** .. autofunction:: normalizeFeatureText *** Lib *** .. autofunction:: normalizeLibKey .. autofunction:: normalizeLibValue ****** Layers ****** .. autofunction:: normalizeLayerOrder .. autofunction:: normalizeDefaultLayer .. autofunction:: normalizeDefaultLayerName .. autofunction:: normalizeLayerName ****** Glyphs ****** .. autofunction:: normalizeGlyph .. autofunction:: normalizeGlyphOrder Identification ============== .. autofunction:: normalizeGlyphName .. autofunction:: normalizeGlyphUnicodes .. autofunction:: normalizeGlyphUnicode Metrics ======= .. autofunction:: normalizeGlyphWidth .. autofunction:: normalizeGlyphLeftMargin .. autofunction:: normalizeGlyphRightMargin .. autofunction:: normalizeGlyphHeight .. autofunction:: normalizeGlyphBottomMargin .. autofunction:: normalizeGlyphTopMargin ******** Contours ******** .. autofunction:: normalizeContourIndex .. autofunction:: normalizeContour ****** Points ****** .. autofunction:: normalizePointType .. autofunction:: normalizePointName ******** Segments ******** .. autofunction:: normalizeSegmentType ******* BPoints ******* .. autofunction:: normalizeBPointType ********** Components ********** .. autofunction:: normalizeComponentIndex .. autofunction:: normalizeComponentScale ******* Anchors ******* .. autofunction:: normalizeAnchorIndex .. autofunction:: normalizeAnchorName **** Note **** .. autofunction:: normalizeGlyphNote ********** Guidelines ********** .. autofunction:: normalizeGuideline .. autofunction:: normalizeGuidelineIndex .. autofunction:: normalizeGuidelineAngle .. autofunction:: normalizeGuidelineName ******* Generic ******* .. autofunction:: normalizeInternalObjectType Positions ========= .. autofunction:: normalizeX .. autofunction:: normalizeY .. autofunction:: normalizeCoordinateTuple .. autofunction:: normalizeBoundingBox Identification ============== .. autofunction:: normalizeIndex .. autofunction:: normalizeIdentifier .. autofunction:: normalizeColor Interpolation ============= .. autofunction:: normalizeInterpolationFactor Transformations =============== .. autofunction:: normalizeTransformationMatrix .. autofunction:: normalizeTransformationOffset .. autofunction:: normalizeTransformationRotationAngle .. autofunction:: normalizeTransformationSkewAngle .. autofunction:: normalizeTransformationScale Files ===== .. autofunction:: normalizeFilePath .. autofunction:: normalizeFileFormatVersion Standard ======== .. autofunction:: normalizeBoolean .. autofunction:: normalizeVisualRounding robotools-fontParts-26e8b8c/documentation/source/environments/objects/point.rst000066400000000000000000000014721477533125200303660ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Point ***** Must Override ------------- .. automethod:: BasePoint._get_identifier .. automethod:: BasePoint._get_name .. automethod:: BasePoint._get_smooth .. automethod:: BasePoint._get_type .. automethod:: BasePoint._get_x .. automethod:: BasePoint._get_y .. automethod:: BasePoint._set_name .. automethod:: BasePoint._set_smooth .. automethod:: BasePoint._set_type .. automethod:: BasePoint._set_x .. automethod:: BasePoint._set_y May Override ------------ .. automethod:: BasePoint._get_index .. automethod:: BasePoint._init .. automethod:: BasePoint._moveBy .. automethod:: BasePoint._rotateBy .. automethod:: BasePoint._round .. automethod:: BasePoint._scaleBy .. automethod:: BasePoint._skewBy .. automethod:: BasePoint._transformBy .. automethod:: BasePoint.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/objects/segment.rst000066400000000000000000000015271477533125200307000ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base Segment ******* Must Override ------------- May Override ------------ .. automethod:: BaseSegment._getItem .. automethod:: BaseSegment._get_base_offCurve .. automethod:: BaseSegment._get_index .. automethod:: BaseSegment._get_offCurve .. automethod:: BaseSegment._get_onCurve .. automethod:: BaseSegment._get_points .. automethod:: BaseSegment._get_smooth .. automethod:: BaseSegment._get_type .. automethod:: BaseSegment._init .. automethod:: BaseSegment._iterPoints .. automethod:: BaseSegment._len .. automethod:: BaseSegment._moveBy .. automethod:: BaseSegment._rotateBy .. automethod:: BaseSegment._scaleBy .. automethod:: BaseSegment._set_smooth .. automethod:: BaseSegment._set_type .. automethod:: BaseSegment._skewBy .. automethod:: BaseSegment._transformBy .. automethod:: BaseSegment.copyDatarobotools-fontParts-26e8b8c/documentation/source/environments/testing/000077500000000000000000000000001477533125200265235ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/environments/testing/index.rst000066400000000000000000000033531477533125200303700ustar00rootroot00000000000000 ####### Testing ####### .. _implementing-testing: A test suite is provided to test any implementation, either subclassed from the base objects or implemented independently. The suite has been designed to be environment and format agnostic. Environment developers only need to implement a function that provides objects for testing and a simple Python script that sends the function to the test suite. Testing an environment ====================== The main thing that an environment needs to implement is the test object generator. This should create an object for the requested class identifier. :: def MyAppObjectGenerator(classIdentifier): unrequested = [] obj = myApp.foo.bar.something.hi(classIdentifier) return obj, unrequested If an environment does not allow orphan objects, parent objects may create the parent objects and store them in a list. The function must return the generated objects and the list of unrequested objects (or an empty list if no parent objects were generated). The class identifiers are as follows: * font * info * groups * kerning * features * lib * layer * glyph * contour * segment * bpoint * point * component * anchor * image * guideline Once an environment has developed this function, all that remains is to pass the function to the test runner:: from fontParts.test import testEnvironment if __name__ == "__main__": testEnvironment(MyAppObjectGenerator) This can then be executed and the report will be printed. .. :note:: It is up to each environment to ensure that the bridge from the environment's native objects to the fontParts wrappers is working properly. This has to be done on an environment by environment basis since the native objects are not consistently implemented.robotools-fontParts-26e8b8c/documentation/source/gettingstarted/000077500000000000000000000000001477533125200253475ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/gettingstarted/index.rst000066400000000000000000000010221477533125200272030ustar00rootroot00000000000000.. highlight:: python ############### Getting Started ############### These need to be ported and updated from RoboFab's documentation. For a quick start, here's the sample code from the introduction ported to fontparts:: from fontParts.world import OpenFont font = OpenFont("/path/to/my/font.ufo") for glyph in font: glyph.leftMargin = glyph.leftMargin + 10 glyph.rightMargin = glyph.rightMargin + 10 Find more of the original samples at https://github.com/robotools/robofab/tree/master/Docs/Examples robotools-fontParts-26e8b8c/documentation/source/index.rst000066400000000000000000000070621477533125200241650ustar00rootroot00000000000000.. highlight:: python History ======= FontParts is based on RoboFab. RoboFab was based on RoboFog. RoboFog was a fork of Fontographer with a built-in Python interpreter. The Fontographer core was wrapped with a lovely Python API. For example, to modify the spacing in all characters ("glyphs" wasn't a standard term back then) in the current font you'd do this:: font = CurrentFont() for character in font: character.leftMargin = character.leftMargin + 10 character.rightMargin = character.rightMargin + 10 When RoboFog could no longer be updated, lots of us designers switched to FontLab. We had *lots* of RoboFog scripts that were critical parts of our workflows and we needed them to work in FontLab right away. FontLab had a built-in Python interpreter, but the API for interacting with the FontLab core was very different from the API in RoboFog. So, a few of us (Erik, Just, Tal) wrote a library called RoboFab that implemented an API that was very similar to the RoboFog API. Designers could take their existing scripts, modify them a tiny bit and they would just work in FontLab. For example, here's how the above script would have been modified:: from robofab.world import CurrentFont font = CurrentFont() for character in font: character.leftMargin = character.leftMargin + 10 character.rightMargin = character.rightMargin + 10 This proved to be incredibly useful and it gave us the idea that a universal, environment independent scripting API would be a very good thing to have. So, we extended RoboFab to work in other environments. For example, to get the above script to work outside of any font editor, you would have done this:: from robofab.world import OpenFont font = OpenFont("/path/to/my/font.ufo") for character in font: character.leftMargin = character.leftMargin + 10 character.rightMargin = character.rightMargin + 10 Did you notice that the important parts of the script are completely unchanged? Sure, this is a simple two line example, but imagine that you have a suite of tools made of hundreds of thousands of lines of code. *Portable APIs are awesome!!!!!!* This was very stable and worked reliably for over a decade. New font editors came along. New font formats came along. New ideas came along. RoboFab was not built in a way that made it easy to add all of these new things while making the old things keep working. We tried, hard, to make it work, but it wasn't possible. We decided that the way forward was to start over from scratch. That idea became FontParts. TL;DR: FontParts is a new implementation of ideas that have worked nearly flawlessly for over two decades. But why isn't it called RoboFab? -------------------------------- Good question. Well, it's not 100% compatible with RoboFab, so we couldn't just drop it in place without breaking some working scripts. So, it needed a new name. Erik came up with the name "FontParts" because, you know, it represents parts of fonts. Design Goals ============ The RoboFog API was quite simple and memorable. FontParts should follow the same model. * It should be easy to understand. The main users of this API will be typeface designers, not professional coders. * The objects, methods, arguments and return values should be memorable. We don't want designers to have to spend a lot of time trying to remember how to do basic things. * It should look Pythonic. Python is a very legible language. That's great, but it can get uglified when ``environmentsStart_wrapping_lowerLevelAPIs``. We want the FontParts API to look like Python code so that it is easy to read. robotools-fontParts-26e8b8c/documentation/source/objectref/000077500000000000000000000000001477533125200242625ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/objectref/fontpartsworld/000077500000000000000000000000001477533125200273525ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/objectref/fontpartsworld/index.rst000066400000000000000000000012451477533125200312150ustar00rootroot00000000000000.. highlight:: python ################ fontParts.world ################ .. module:: fontParts.world .. note:: We still need to decide if we need a ``world`` module or if we should recommend namespace injection. .. autofunction:: AllFonts .. autofunction:: NewFont .. autofunction:: OpenFont .. autofunction:: OpenFonts .. autofunction:: CurrentFont .. autofunction:: CurrentLayer .. autofunction:: CurrentGlyph .. autofunction:: CurrentContours .. autofunction:: CurrentSegments .. autofunction:: CurrentPoints .. autofunction:: CurrentComponents .. autofunction:: CurrentAnchors .. autofunction:: CurrentGuidelines .. autofunction:: FontList .. autoclass:: BaseFontListrobotools-fontParts-26e8b8c/documentation/source/objectref/index.rst000066400000000000000000000005301477533125200261210ustar00rootroot00000000000000.. highlight:: python ################ Object Reference ################ FontParts scripts are built on with objects that represent fonts, glyphs, contours and so on. The objects are obtained through :ref:`fontparts-world`. .. toctree:: :maxdepth: 2 :includehidden: objects/index valuetypes/index fontpartsworld/index robotools-fontParts-26e8b8c/documentation/source/objectref/objects/000077500000000000000000000000001477533125200257135ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/objectref/objects/anchor.rst000066400000000000000000000040211477533125200277140ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ###### Anchor ###### *********** Description *********** Anchors are single points in a glyph which are not part of a contour. They can be used as reference positions for doing things like assembling components. In most font editors, anchors have a special appearance and can be edited. :: glyph = CurrentGlyph() for anchor in glyph.anchors: print(anchor) ******** Overview ******** Copy ==== .. autosummary:: :nosignatures: BaseAnchor.copy Parents ======= .. autosummary:: :nosignatures: BaseAnchor.glyph BaseAnchor.layer BaseAnchor.font Identification ============== .. autosummary:: :nosignatures: BaseAnchor.name BaseAnchor.color BaseAnchor.identifier BaseAnchor.index Coordinate ========== .. autosummary:: :nosignatures: BaseAnchor.x BaseAnchor.y Transformations =============== .. autosummary:: :nosignatures: BaseAnchor.transformBy BaseAnchor.moveBy BaseAnchor.scaleBy BaseAnchor.rotateBy BaseAnchor.skewBy Normalization ============= .. autosummary:: :nosignatures: BaseAnchor.round Environment =========== .. autosummary:: :nosignatures: BaseAnchor.naked BaseAnchor.changed ********* Reference ********* .. autoclass:: BaseAnchor Copy ==== .. automethod:: BaseAnchor.copy Parents ======= .. autoattribute:: BaseAnchor.glyph .. autoattribute:: BaseAnchor.layer .. autoattribute:: BaseAnchor.font Identification ============== .. autoattribute:: BaseAnchor.name .. autoattribute:: BaseAnchor.color .. autoattribute:: BaseAnchor.identifier .. autoattribute:: BaseAnchor.index Coordinate ========== .. autoattribute:: BaseAnchor.x .. autoattribute:: BaseAnchor.y Transformations =============== .. automethod:: BaseAnchor.transformBy .. automethod:: BaseAnchor.moveBy .. automethod:: BaseAnchor.scaleBy .. automethod:: BaseAnchor.rotateBy .. automethod:: BaseAnchor.skewBy Normalization ============= .. automethod:: BaseAnchor.round Environment =========== .. automethod:: BaseAnchor.naked .. automethod:: BaseAnchor.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/bpoint.rst000066400000000000000000000045411477533125200277440ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ###### bPoint ###### *********** Description *********** The :class:`bPoint ` is a point object which mimics the old “Bezier Point” from RoboFog. It has attributes for :attr:`bcpIn `, anchor, bcpOut and type. The coordinates in bcpIn and bcpOut are relative to the position of the anchor. For instance, if the bcpIn is 20 units to the left of the anchor, its coordinates would be (-20,0), regardless of the coordinates of the anchor itself. Also: bcpIn will be (0,0) when it is “on top of the anchor”, i.e. when there is no bcp it will still have a value. The parent of a bPoint is usually a :class:`Contour `. ******** Overview ******** Parents ======= .. autosummary:: :nosignatures: BaseBPoint.contour BaseBPoint.glyph BaseBPoint.layer BaseBPoint.font Identification ============== .. autosummary:: :nosignatures: BaseBPoint.index Attributes ========== .. autosummary:: :nosignatures: BaseBPoint.type Points ====== .. autosummary:: :nosignatures: BaseBPoint.anchor BaseBPoint.bcpIn BaseBPoint.bcpOut Transformations =============== .. autosummary:: :nosignatures: BaseBPoint.transformBy BaseBPoint.moveBy BaseBPoint.scaleBy BaseBPoint.rotateBy BaseBPoint.skewBy Normalization ============= .. autosummary:: :nosignatures: BaseBPoint.round Environment =========== .. autosummary:: :nosignatures: BaseBPoint.naked BaseBPoint.changed ********* Reference ********* .. autoclass:: BaseBPoint Parents ======= .. autoattribute:: BaseBPoint.contour .. autoattribute:: BaseBPoint.glyph .. autoattribute:: BaseBPoint.layer .. autoattribute:: BaseBPoint.font Identification ============== .. autoattribute:: BaseBPoint.index Attributes ========== .. autoattribute:: BaseBPoint.type Points ====== .. autoattribute:: BaseBPoint.anchor .. autoattribute:: BaseBPoint.bcpIn .. autoattribute:: BaseBPoint.bcpOut Transformations =============== .. automethod:: BaseBPoint.transformBy .. automethod:: BaseBPoint.moveBy .. automethod:: BaseBPoint.scaleBy .. automethod:: BaseBPoint.rotateBy .. automethod:: BaseBPoint.skewBy Normalization ============= .. automethod:: BaseBPoint.round Environment =========== .. automethod:: BaseBPoint.naked .. automethod:: BaseBPoint.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/component.rst000066400000000000000000000057331477533125200304570ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ######### Component ######### *********** Description *********** A component can be a part of a glyph, and it is a reference to another glyph in the same font. With components you can make glyphs depend on other glyphs. Changes to the base glyph will reflect in the component as well. The parent of a component is usually a glyph. Components can be decomposed: they replace themselves with the actual outlines from the base glyph. When that happens, the link between the original and the component is broken: changes to the base glyph will no longer reflect in the glyph that had the component. ******** Overview ******** Parents ======= .. autosummary:: :nosignatures: BaseComponent.glyph BaseComponent.layer BaseComponent.font Copy ==== .. autosummary:: :nosignatures: BaseComponent.copy Identification ============== .. autosummary:: :nosignatures: BaseComponent.identifier BaseComponent.index Attributes ========== .. autosummary:: :nosignatures: BaseComponent.baseGlyph BaseComponent.transformation BaseComponent.offset BaseComponent.scale Queries ======= .. autosummary:: :nosignatures: BaseComponent.bounds BaseComponent.pointInside Pens and Drawing ================ .. autosummary:: :nosignatures: BaseComponent.draw BaseComponent.drawPoints Transformations =============== .. autosummary:: :nosignatures: BaseComponent.transformBy BaseComponent.moveBy BaseComponent.scaleBy BaseComponent.rotateBy BaseComponent.skewBy Normalization ============= .. autosummary:: :nosignatures: BaseComponent.decompose BaseComponent.round Environment =========== .. autosummary:: :nosignatures: BaseComponent.naked BaseComponent.changed ********* Reference ********* .. autoclass:: BaseComponent Parents ======= .. autoattribute:: BaseComponent.glyph .. autoattribute:: BaseComponent.layer .. autoattribute:: BaseComponent.font Copy ==== .. automethod:: BaseComponent.copy Identification ============== .. autoattribute:: BaseComponent.identifier .. autoattribute:: BaseComponent.index Attributes ========== .. autoattribute:: BaseComponent.baseGlyph .. autoattribute:: BaseComponent.transformation .. autoattribute:: BaseComponent.offset .. autoattribute:: BaseComponent.scale Queries ======= .. autoattribute:: BaseComponent.bounds .. automethod:: BaseComponent.pointInside Pens and Drawing ================ .. automethod:: BaseComponent.draw .. automethod:: BaseComponent.drawPoints Transformations =============== .. automethod:: BaseComponent.transformBy .. automethod:: BaseComponent.moveBy .. automethod:: BaseComponent.scaleBy .. automethod:: BaseComponent.rotateBy .. automethod:: BaseComponent.skewBy Normalization ============= .. automethod:: BaseComponent.decompose .. automethod:: BaseComponent.round Environment =========== .. automethod:: BaseComponent.naked .. automethod:: BaseComponent.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/contour.rst000066400000000000000000000074411477533125200301440ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ####### Contour ####### *********** Description *********** A Contour is a single path of any number of points. A Glyph usually consists of a couple of contours, and this is the object that represents each one. The :class:`Contour ` object offers access to the outline matter in various ways. The parent of :class:`Contour ` is usually :class:`Glyph `. ******** Overview ******** Copy ==== .. autosummary:: :nosignatures: BaseContour.copy Parents ======= .. autosummary:: :nosignatures: BaseContour.glyph BaseContour.layer BaseContour.font Identification ============== .. autosummary:: :nosignatures: BaseContour.identifier BaseContour.index Winding Direction ================= .. autosummary:: :nosignatures: BaseContour.clockwise BaseContour.reverse Queries ======= .. autosummary:: :nosignatures: BaseContour.bounds BaseContour.pointInside Pens and Drawing ================ .. autosummary:: :nosignatures: BaseContour.draw BaseContour.drawPoints Segments ======== .. autosummary:: :nosignatures: BaseContour.segments BaseContour.__len__ BaseContour.__iter__ BaseContour.__getitem__ BaseContour.appendSegment BaseContour.insertSegment BaseContour.removeSegment BaseContour.setStartSegment BaseContour.autoStartSegment bPoints ======= .. autosummary:: :nosignatures: BaseContour.bPoints BaseContour.appendBPoint BaseContour.insertBPoint Points ====== .. autosummary:: :nosignatures: BaseContour.points BaseContour.appendPoint BaseContour.insertPoint BaseContour.removePoint Transformations =============== .. autosummary:: :nosignatures: BaseContour.transformBy BaseContour.moveBy BaseContour.scaleBy BaseContour.rotateBy BaseContour.skewBy Normalization ============= .. autosummary:: :nosignatures: BaseContour.round Environment =========== .. autosummary:: :nosignatures: BaseContour.naked BaseContour.changed ********* Reference ********* .. autoclass:: BaseContour Copy ==== .. automethod:: BaseContour.copy Parents ======= .. autoattribute:: BaseContour.glyph .. autoattribute:: BaseContour.layer .. autoattribute:: BaseContour.font Identification ============== .. autoattribute:: BaseContour.identifier .. autoattribute:: BaseContour.index Winding Direction ================= .. autoattribute:: BaseContour.clockwise .. automethod:: BaseContour.reverse Queries ======= .. autoattribute:: BaseContour.bounds .. automethod:: BaseContour.pointInside Pens and Drawing ================ .. automethod:: BaseContour.draw .. automethod:: BaseContour.drawPoints Segments ======== .. autoattribute:: BaseContour.segments .. automethod:: BaseContour.__len__ .. automethod:: BaseContour.__iter__ .. automethod:: BaseContour.__getitem__ .. automethod:: BaseContour.appendSegment .. automethod:: BaseContour.insertSegment .. automethod:: BaseContour.removeSegment .. automethod:: BaseContour.setStartSegment .. automethod:: BaseContour.autoStartSegment bPoints ======= .. autoattribute:: BaseContour.bPoints .. automethod:: BaseContour.appendBPoint .. automethod:: BaseContour.insertBPoint Points ====== .. autoattribute:: BaseContour.points .. automethod:: BaseContour.appendPoint .. automethod:: BaseContour.insertPoint .. automethod:: BaseContour.removePoint Transformations =============== .. automethod:: BaseContour.transformBy .. automethod:: BaseContour.moveBy .. automethod:: BaseContour.scaleBy .. automethod:: BaseContour.rotateBy .. automethod:: BaseContour.skewBy Normalization ============= .. automethod:: BaseContour.round Environment =========== .. automethod:: BaseContour.naked .. automethod:: BaseContour.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/features.rst000066400000000000000000000025651477533125200302730ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ######## Features ######## *********** Description *********** Features is text in the `Adobe Font Development Kit `_ for OpenType `.fea syntax `_ that describes the OpenType features of your font. The `OpenType Cookbook `_ is a great place to start learning how to write features. Your features must be self-contained; for example, any glyph or mark classes must be defined within the file. No assumption should be made about the validity of the syntax, and FontParts does not check the validity of the syntax. .. note:: It is important to note that the features file may contain data that is a duplicate of or data that is in conflict with the data in :class:`BaseKerning`, :class:`BaseGroups`, and :class:`BaseInfo`. Synchronization is up to the user and application developers. :: font = CurrentFont() print(font.features) ******** Overview ******** .. autosummary:: :nosignatures: BaseFeatures.copy BaseFeatures.font BaseFeatures.text ********* Reference ********* .. autoclass:: BaseFeatures Copy ==== .. automethod:: BaseFeatures.copy Parents ======= .. autoattribute:: BaseFeatures.font Attributes ========== .. autoattribute:: BaseFeatures.text robotools-fontParts-26e8b8c/documentation/source/objectref/objects/font.rst000066400000000000000000000074161477533125200274230ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base #### Font #### .. note:: This section needs to contain the following: * description of what this is ✓ * sub-object with basic usage ✓ * bridge to default layer for glyphs for backwards compatibility ✗ * glyph interaction with basic usage ✗ *********** Description *********** The :class:`Font ` object is the central part that connects all glyphs with font information like names, key dimensions etc. :class:`Font ` objects behave like dictionaries: the glyph name is the key and the returned value is a :class:`Glyph ` object for that glyph. If the glyph does not exist, :class:`Font ` will raise an ``IndexError``. :class:`Font ` has a couple of important sub-objects which are worth checking out. The font’s kerning is stored in a :class:`Kerning ` object and can be reached as an attribute at ``Font.kerning``. Fontnames, key dimensions, flags etc are stored in a :class:`Info ` object which is available through ``Font.info``. The ``Font.lib`` is a :class:`Lib ` object which behaves as a dictionary. ******** Overview ******** Copy ==== .. autosummary:: :nosignatures: BaseFont.copy File Operations =============== .. autosummary:: :nosignatures: BaseFont.path BaseFont.save BaseFont.generate Sub-Objects =========== .. autosummary:: :nosignatures: BaseFont.info BaseFont.groups BaseFont.kerning BaseFont.features BaseFont.lib BaseFont.tempLib Layers ====== .. autosummary:: :nosignatures: BaseFont.layers BaseFont.layerOrder BaseFont.defaultLayer BaseFont.getLayer BaseFont.newLayer BaseFont.removeLayer BaseFont.insertLayer BaseFont.duplicateLayer Glyphs ====== .. autosummary:: :nosignatures: BaseFont.__len__ BaseFont.keys BaseFont.glyphOrder BaseFont.__iter__ BaseFont.__contains__ BaseFont.__getitem__ BaseFont.newGlyph BaseFont.insertGlyph BaseFont.removeGlyph ********* Reference ********* .. autoclass:: BaseFont Copy ==== .. automethod:: BaseFont.copy File Operations =============== .. autoattribute:: BaseFont.path .. automethod:: BaseFont.save .. automethod:: BaseFont.close .. automethod:: BaseFont.generate Sub-Objects =========== .. autoattribute:: BaseFont.info .. autoattribute:: BaseFont.groups .. autoattribute:: BaseFont.kerning .. autoattribute:: BaseFont.features .. autoattribute:: BaseFont.lib Layers ====== .. autoattribute:: BaseFont.layers .. autoattribute:: BaseFont.layerOrder .. autoattribute:: BaseFont.defaultLayer .. automethod:: BaseFont.getLayer .. automethod:: BaseFont.newLayer .. automethod:: BaseFont.removeLayer .. automethod:: BaseFont.insertLayer Glyphs ====== Interacting with glyphs at the font level is a shortcut for interacting with glyphs in the default layer. :: >>> glyph = font.newGlyph("A") Does the same thing as:: >>> glyph = font.getLayer(font.defaultLayerName).newGlyph("A") .. automethod:: BaseFont.__len__ .. automethod:: BaseFont.keys .. autoattribute:: BaseFont.glyphOrder .. automethod:: BaseFont.__iter__ .. automethod:: BaseFont.__contains__ .. automethod:: BaseFont.__getitem__ .. automethod:: BaseFont.newGlyph .. automethod:: BaseFont.insertGlyph .. automethod:: BaseFont.removeGlyph Guidelines ========== .. autoattribute:: BaseFont.guidelines .. automethod:: BaseFont.appendGuideline .. automethod:: BaseFont.removeGuideline .. automethod:: BaseFont.clearGuidelines Interpolation ============= .. automethod:: BaseFont.isCompatible .. automethod:: BaseFont.interpolate Normalization ============= .. automethod:: BaseFont.round .. automethod:: BaseFont.autoUnicodes Environment =========== .. automethod:: BaseFont.naked .. automethod:: BaseFont.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/glyph.rst000066400000000000000000000142241477533125200275730ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ##### Glyph ##### .. autosummarymethodlist:: BaseGlyph *********** Description *********** The :class:`Glyph ` object represents a glyph, its parts and associated data. :class:`Glyph ` can be used as a list of :class:`Contour ` objects. When a :class:`Glyph ` is obtained from a :class:`Font ` object, the font is the parent object of the glyph. ******** Overview ******** Copy ==== .. autosummary:: :nosignatures: BaseGlyph.copy Parents ======= .. autosummary:: :nosignatures: BaseGlyph.layer BaseGlyph.font Identification ============== .. autosummary:: :nosignatures: BaseGlyph.name BaseGlyph.unicodes BaseGlyph.unicode Metrics ======= .. autosummary:: :nosignatures: BaseGlyph.width BaseGlyph.leftMargin BaseGlyph.rightMargin BaseGlyph.height BaseGlyph.bottomMargin BaseGlyph.topMargin Queries ======= .. autosummary:: :nosignatures: BaseGlyph.bounds BaseGlyph.pointInside Pens and Drawing ================ .. autosummary:: :nosignatures: BaseGlyph.getPen BaseGlyph.getPointPen BaseGlyph.draw BaseGlyph.drawPoints Layers ====== .. autosummary:: :nosignatures: BaseGlyph.layers BaseGlyph.getLayer BaseGlyph.newLayer BaseGlyph.removeLayer Global ====== .. autosummary:: :nosignatures: BaseGlyph.clear BaseGlyph.appendGlyph Contours ======== .. autosummary:: :nosignatures: BaseGlyph.contours BaseGlyph.__len__ BaseGlyph.__iter__ BaseGlyph.__getitem__ BaseGlyph.appendContour BaseGlyph.removeContour BaseGlyph.clearContours BaseGlyph.removeOverlap Components ========== .. autosummary:: :nosignatures: BaseGlyph.components BaseGlyph.appendComponent BaseGlyph.removeComponent BaseGlyph.clearComponents BaseGlyph.decompose Anchors ======= .. autosummary:: :nosignatures: BaseGlyph.anchors BaseGlyph.appendAnchor BaseGlyph.removeAnchor BaseGlyph.clearAnchors Guidelines ========== .. autosummary:: :nosignatures: BaseGlyph.guidelines BaseGlyph.appendGuideline BaseGlyph.removeGuideline BaseGlyph.clearGuidelines Image ===== .. autosummary:: :nosignatures: BaseGlyph.image BaseGlyph.addImage BaseGlyph.clearImage Note ==== .. autosummary:: :nosignatures: BaseGlyph.note BaseGlyph.markColor Sub-Objects =========== .. autosummary:: :nosignatures: BaseGlyph.lib BaseGlyph.tempLib Transformations =============== .. autosummary:: :nosignatures: BaseGlyph.transformBy BaseGlyph.moveBy BaseGlyph.scaleBy BaseGlyph.rotateBy BaseGlyph.skewBy Interpolation ============= .. autosummary:: :nosignatures: BaseGlyph.isCompatible BaseGlyph.interpolate Normalization ============= .. autosummary:: :nosignatures: BaseGlyph.round BaseGlyph.autoUnicodes Environment =========== .. autosummary:: :nosignatures: BaseGlyph.naked BaseGlyph.changed ********* Reference ********* .. autoclass:: BaseGlyph Copy ==== .. automethod:: BaseGlyph.copy Parents ======= .. autoattribute:: BaseGlyph.layer .. autoattribute:: BaseGlyph.font Identification ============== .. autoattribute:: BaseGlyph.name .. autoattribute:: BaseGlyph.unicodes .. autoattribute:: BaseGlyph.unicode Metrics ======= .. autoattribute:: BaseGlyph.width .. autoattribute:: BaseGlyph.leftMargin .. autoattribute:: BaseGlyph.rightMargin .. autoattribute:: BaseGlyph.height .. autoattribute:: BaseGlyph.bottomMargin .. autoattribute:: BaseGlyph.topMargin Queries ======= .. autoattribute:: BaseGlyph.bounds .. automethod:: BaseGlyph.pointInside Pens and Drawing ================ .. automethod:: BaseGlyph.getPen .. automethod:: BaseGlyph.getPointPen .. automethod:: BaseGlyph.draw .. automethod:: BaseGlyph.drawPoints Layers ====== Layer interaction in glyphs is very similar to the layer interaction in fonts. When you ask a glyph for a layer, you get a *glyph layer* in return. A glyph layer lets you do anything that you can do to a glyph. In fact a glyph layer is really just a glyph. >>> bgdGlyph = glyph.newLayer('background') >>> bgdGlyph.appendGlyph(glyph) >>> bgdGlyph.appendGuideline((10, 10), 45) .. autoattribute:: BaseGlyph.layers .. automethod:: BaseGlyph.getLayer .. automethod:: BaseGlyph.newLayer .. automethod:: BaseGlyph.removeLayer Global ====== .. automethod:: BaseGlyph.clear .. automethod:: BaseGlyph.appendGlyph Contours ======== .. autoattribute:: BaseGlyph.contours .. automethod:: BaseGlyph.__len__ .. automethod:: BaseGlyph.__iter__ .. automethod:: BaseGlyph.__getitem__ .. automethod:: BaseGlyph.appendContour .. automethod:: BaseGlyph.removeContour .. automethod:: BaseGlyph.clearContours .. automethod:: BaseGlyph.removeOverlap Components ========== .. autoattribute:: BaseGlyph.components .. automethod:: BaseGlyph.appendComponent .. automethod:: BaseGlyph.removeComponent .. automethod:: BaseGlyph.clearComponents .. automethod:: BaseGlyph.decompose Anchors ======= .. autoattribute:: BaseGlyph.anchors .. automethod:: BaseGlyph.appendAnchor .. automethod:: BaseGlyph.removeAnchor .. automethod:: BaseGlyph.clearAnchors Guidelines ========== .. autoattribute:: BaseGlyph.guidelines .. automethod:: BaseGlyph.appendGuideline .. automethod:: BaseGlyph.removeGuideline .. automethod:: BaseGlyph.clearGuidelines Image ===== .. autoattribute:: BaseGlyph.image .. automethod:: BaseGlyph.addImage .. automethod:: BaseGlyph.clearImage Note ==== .. autoattribute:: BaseGlyph.note .. autoattribute:: BaseGlyph.markColor Sub-Objects =========== .. autoattribute:: BaseGlyph.lib Transformations =============== .. automethod:: BaseGlyph.transformBy .. automethod:: BaseGlyph.moveBy .. automethod:: BaseGlyph.scaleBy .. automethod:: BaseGlyph.rotateBy .. automethod:: BaseGlyph.skewBy Interpolation ============= .. automethod:: BaseGlyph.isCompatible .. automethod:: BaseGlyph.interpolate Normalization ============= .. automethod:: BaseGlyph.round .. automethod:: BaseGlyph.autoUnicodes Environment =========== .. automethod:: BaseGlyph.naked .. automethod:: BaseGlyph.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/groups.rst000066400000000000000000000071601477533125200277700ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ###### Groups ###### *********** Description *********** Groups are collections of glyphs. Groups are used for many things, from OpenType features, kerning, or just keeping track of a collection of related glyphs. The name of the group must be at least one character, with no limit to the maximum length for the name, nor any limit on the characters used in a name. With the exception of the kerning groups defined below, glyphs may be in more than one group and they may appear within the same group more than once. Glyphs in the groups are not required to be in the font. Groups behave like a Python dictionary. Anything you can do with a dictionary in Python, you can do with Groups. :: font = CurrentFont() for name, members in font.groups.items(): print(name) print(members) It is important to understand that any changes to the returned group contents will not be reflected in the groups object. This means that the following will not update the font's groups: :: group = list(font.groups["myGroup"]) group.remove("A") If one wants to make a change to the group contents, one should do the following instead: :: group = list(font.groups["myGroup"]) group.remove("A") font.groups["myGroup"] = group Kerning Groups ============== Groups may be used as members of kerning pairs in :class:`BaseKerning`. These groups are divided into two types: groups that appear on the first side of a kerning pair and groups that appear on the second side of a kerning pair. Kerning groups must begin with standard prefixes. The prefix for groups intended for use in the first side of a kerning pair is ``public.kern1.``. The prefix for groups intended for use in the second side of a kerning pair is ``public.kern2.``. One or more characters must follow the prefix. Kerning groups must strictly adhere to the following rules: #. Kerning group names must begin with the appropriate prefix. #. Only kerning groups are allowed to use the kerning group prefixes in their names. #. Kerning groups are not required to appear in the kerning pairs. #. Glyphs must not appear in more than one kerning group per side. These rules come from the `Unified Font Object `_, more information on implementation details for application developers can be found there. ******** Overview ******** .. autosummary:: :nosignatures: BaseGroups.copy BaseGroups.font BaseGroups.__contains__ BaseGroups.__delitem__ BaseGroups.__getitem__ BaseGroups.__iter__ BaseGroups.__len__ BaseGroups.__setitem__ BaseGroups.clear BaseGroups.get BaseGroups.items BaseGroups.keys BaseGroups.pop BaseGroups.update BaseGroups.values BaseGroups.findGlyph BaseGroups.naked BaseGroups.changed ********* Reference ********* .. autoclass:: BaseGroups Copy ==== .. automethod:: BaseGroups.copy Parents ======= * :attr:`~BaseGroups.font` The groups' parent :class:`BaseFont`. Dictionary ========== .. automethod:: BaseGroups.__contains__ .. automethod:: BaseGroups.__delitem__ .. automethod:: BaseGroups.__getitem__ .. automethod:: BaseGroups.__iter__ .. automethod:: BaseGroups.__len__ .. automethod:: BaseGroups.__setitem__ .. automethod:: BaseGroups.clear .. automethod:: BaseGroups.get .. automethod:: BaseGroups.items .. automethod:: BaseGroups.keys .. automethod:: BaseGroups.pop .. automethod:: BaseGroups.update .. automethod:: BaseGroups.values Queries ======= .. automethod:: BaseGroups.findGlyph Environment =========== .. automethod:: BaseGroups.naked .. automethod:: BaseGroups.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/guideline.rst000066400000000000000000000050751477533125200304210ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ######### Guideline ######### *********** Description *********** Guidelines are reference lines in a glyph that are not part of a contour or the generated font data. They are defined by a point and an angle; the guideline extends from the point in both directions on the specified angle. They are most often used to keep track of design information for a font ('my overshoots should be here') or to measure positions in a glyph ('line the ends of my serifs on this line'). They can also be used as reference positions for doing things like assembling components. In most font editors, guidelines have a special appearance and can be edited. :: glyph = CurrentGlyph() for guideline in glyph.guidelines: print(guideline) ******** Overview ******** Copy ==== .. autosummary:: :nosignatures: BaseGuideline.copy Parents ======= .. autosummary:: :nosignatures: BaseGuideline.glyph BaseGuideline.layer BaseGuideline.font Identification ============== .. autosummary:: :nosignatures: BaseGuideline.name BaseGuideline.color BaseGuideline.identifier BaseGuideline.index Attributes ========== .. autosummary:: :nosignatures: BaseGuideline.x BaseGuideline.y BaseGuideline.angle Transformations =============== .. autosummary:: :nosignatures: BaseGuideline.transformBy BaseGuideline.moveBy BaseGuideline.scaleBy BaseGuideline.rotateBy BaseGuideline.skewBy Normalization ============= .. autosummary:: :nosignatures: BaseGuideline.round Environment =========== .. autosummary:: :nosignatures: BaseGuideline.naked BaseGuideline.changed ********* Reference ********* .. autoclass:: BaseGuideline Copy ==== .. automethod:: BaseGuideline.copy Parents ======= .. autoattribute:: BaseGuideline.glyph .. autoattribute:: BaseGuideline.layer .. autoattribute:: BaseGuideline.font Identification ============== .. autoattribute:: BaseGuideline.name .. autoattribute:: BaseGuideline.color .. autoattribute:: BaseGuideline.identifier .. autoattribute:: BaseGuideline.index Attributes ========== .. autoattribute:: BaseGuideline.x .. autoattribute:: BaseGuideline.y .. autoattribute:: BaseGuideline.angle Transformations =============== .. automethod:: BaseGuideline.transformBy .. automethod:: BaseGuideline.moveBy .. automethod:: BaseGuideline.scaleBy .. automethod:: BaseGuideline.rotateBy .. automethod:: BaseGuideline.skewBy Normalization ============= .. automethod:: BaseGuideline.round Environment =========== .. automethod:: BaseGuideline.naked .. automethod:: BaseGuideline.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/image.rst000066400000000000000000000024101477533125200275240ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ##### Image ##### ******** Overview ******** .. autosummary:: :nosignatures: BaseImage.copy BaseImage.glyph BaseImage.layer BaseImage.font BaseImage.data BaseImage.color BaseImage.transformation BaseImage.offset BaseImage.scale BaseImage.transformBy BaseImage.moveBy BaseImage.scaleBy BaseImage.rotateBy BaseImage.skewBy BaseImage.round BaseImage.naked BaseImage.changed ********* Reference ********* .. autoclass:: BaseImage Copy ==== .. automethod:: BaseImage.copy Parents ======= .. autoattribute:: BaseImage.glyph .. autoattribute:: BaseImage.layer .. autoattribute:: BaseImage.font Attributes ========== .. autoattribute:: BaseImage.data .. autoattribute:: BaseImage.color .. autoattribute:: BaseImage.transformation .. autoattribute:: BaseImage.offset .. autoattribute:: BaseImage.scale Transformations =============== .. automethod:: BaseImage.transformBy .. automethod:: BaseImage.moveBy .. automethod:: BaseImage.scaleBy .. automethod:: BaseImage.rotateBy .. automethod:: BaseImage.skewBy Normalization ============= .. automethod:: BaseImage.round Environment =========== .. automethod:: BaseImage.naked .. automethod:: BaseImage.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/index.rst000066400000000000000000000332751477533125200275660ustar00rootroot00000000000000.. highlight:: python ################ Objects ################ FontParts scripts are built on with objects that represent fonts, glyphs, contours and so on. The objects are obtained through :ref:`fontparts-world`. .. toctree:: :maxdepth: 1 :hidden: font info groups kerning features lib layer glyph contour segment bpoint point component anchor image guideline .. _fontparts-objects: .. raw:: html info layer features point image component lib contour guideline kerning lib font segment anchor bPoint glyph robotools-fontParts-26e8b8c/documentation/source/objectref/objects/info.rst000066400000000000000000000027641477533125200274110ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base #### Info #### *********** Description *********** The :class:`Info ` object contains all names, numbers, URLs, dimensions, values, etc. that would otherwise clutter up the font object. You don't have to create a :class:`Info ` object yourself, :class:`Font ` makes one when it is created. :class:`Info ` validates any value set for a `Info ` item, but does not check if the data is sane (i.e., you can set valid but incorrect data). The :class:`Info ` object (as any other fontParts object) does not allow to modify mutable containers (like lists) in-place. Always get a value, modify it and then set it back to perform an edit. For a list of info attributes, refer to the `UFO fontinfo.plist Specification `_. ******** Overview ******** .. autosummary:: :nosignatures: BaseInfo.copy BaseInfo.font BaseInfo.interpolate BaseInfo.round BaseInfo.update BaseInfo.naked BaseInfo.changed ********* Reference ********* .. autoclass:: BaseInfo Copy ==== .. automethod:: BaseInfo.copy Parents ======= .. autoattribute:: BaseInfo.font Interpolation ============= .. automethod:: BaseInfo.interpolate Normalization ============= .. automethod:: BaseInfo.round Update ====== .. automethod:: BaseInfo.update Environment =========== .. automethod:: BaseInfo.naked .. automethod:: BaseInfo.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/kerning.rst000066400000000000000000000055111477533125200301040ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ####### Kerning ####### *********** Description *********** Kerning groups must begin with standard prefixes. The prefix for groups intended for use in the first side of a kerning pair is ``public.kern1.``. The prefix for groups intended for use in the second side of a kerning pair is ``public.kern2.``. One or more characters must follow the prefix. Kerning groups must strictly adhere to the following rules: #. Kerning group names must begin with the appropriate prefix. #. Only kerning groups are allowed to use the kerning group prefixes in their names. #. Kerning groups are not required to appear in the kerning pairs. #. Glyphs must not appear in more than one kerning group per side. These rules come from the `Unified Font Object `_, more information on implementation details for application developers can be found there. ******** Overview ******** Copy ==== .. autosummary:: :nosignatures: BaseKerning.copy Parents ======= .. autosummary:: :nosignatures: BaseKerning.font Dictionary ========== .. autosummary:: :nosignatures: BaseKerning.__len__ BaseKerning.keys BaseKerning.items BaseKerning.values BaseKerning.__contains__ BaseKerning.__setitem__ BaseKerning.__getitem__ BaseKerning.get BaseKerning.find BaseKerning.__delitem__ BaseKerning.pop BaseKerning.__iter__ BaseKerning.update BaseKerning.clear Transformations =============== .. autosummary:: :nosignatures: BaseKerning.scaleBy Interpolation ============= .. autosummary:: :nosignatures: BaseKerning.interpolate Normalization ============= .. autosummary:: :nosignatures: BaseKerning.round Environment =========== .. autosummary:: :nosignatures: BaseKerning.naked BaseKerning.changed ********* Reference ********* .. autoclass:: BaseKerning Copy ==== .. automethod:: BaseKerning.copy Parents ======= .. autoattribute:: BaseKerning.font Dictionary ========== .. automethod:: BaseKerning.__len__ .. automethod:: BaseKerning.keys .. automethod:: BaseKerning.items .. automethod:: BaseKerning.values .. automethod:: BaseKerning.__contains__ .. automethod:: BaseKerning.__setitem__ .. automethod:: BaseKerning.__getitem__ .. automethod:: BaseKerning.get .. automethod:: BaseKerning.find .. automethod:: BaseKerning.__delitem__ .. automethod:: BaseKerning.pop .. automethod:: BaseKerning.__iter__ .. automethod:: BaseKerning.update .. automethod:: BaseKerning.clear Transformations =============== .. automethod:: BaseKerning.scaleBy Interpolation ============= .. automethod:: BaseKerning.interpolate Normalization ============= .. automethod:: BaseKerning.round Environment =========== .. automethod:: BaseKerning.naked .. automethod:: BaseKerning.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/layer.rst000066400000000000000000000040551477533125200275650ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ##### Layer ##### .. note:: This section needs to contain the following: * description of what this is * sub-object with basic usage * glyph interaction with basic usage ******** Overview ******** Copy ==== .. autosummary:: :nosignatures: BaseLayer.copy Parents ======= .. autosummary:: :nosignatures: BaseLayer.font Attributes ========== .. autosummary:: :nosignatures: BaseLayer.name BaseLayer.color Sub-Objects =========== .. autosummary:: :nosignatures: BaseLayer.lib BaseLayer.tempLib Glyphs ====== .. autosummary:: :nosignatures: BaseLayer.__len__ BaseLayer.keys BaseLayer.__iter__ BaseLayer.__contains__ BaseLayer.__getitem__ BaseLayer.newGlyph BaseLayer.insertGlyph BaseLayer.removeGlyph Interpolation ============= .. autosummary:: :nosignatures: BaseLayer.isCompatible BaseLayer.interpolate Normalization ============= .. autosummary:: :nosignatures: BaseLayer.round BaseLayer.autoUnicodes Environment =========== .. autosummary:: :nosignatures: BaseLayer.naked BaseLayer.changed ********* Reference ********* .. autoclass:: BaseLayer Copy ==== .. automethod:: BaseLayer.copy Parents ======= .. autoattribute:: BaseLayer.font Attributes ========== .. autoattribute:: BaseLayer.name .. autoattribute:: BaseLayer.color Sub-Objects =========== .. autoattribute:: BaseLayer.lib Glyphs ====== .. automethod:: BaseLayer.__len__ .. automethod:: BaseLayer.keys .. automethod:: BaseLayer.__iter__ .. automethod:: BaseLayer.__contains__ .. automethod:: BaseLayer.__getitem__ .. automethod:: BaseLayer.newGlyph .. automethod:: BaseLayer.insertGlyph .. automethod:: BaseLayer.removeGlyph Interpolation ============= .. automethod:: BaseLayer.isCompatible .. automethod:: BaseLayer.interpolate Normalization ============= .. automethod:: BaseLayer.round .. automethod:: BaseLayer.autoUnicodes Environment =========== .. automethod:: BaseLayer.naked .. automethod:: BaseLayer.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/lib.rst000066400000000000000000000022441477533125200272150ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ### Lib ### ******** Overview ******** .. autosummary:: :nosignatures: BaseLib.copy BaseLib.glyph BaseLib.font BaseLib.__len__ BaseLib.keys BaseLib.items BaseLib.values BaseLib.__contains__ BaseLib.__setitem__ BaseLib.__getitem__ BaseLib.get BaseLib.__delitem__ BaseLib.pop BaseLib.__iter__ BaseLib.update BaseLib.clear BaseLib.naked BaseLib.changed ********* Reference ********* .. autoclass:: BaseLib Copy ==== .. automethod:: BaseLib.copy Parents ======= .. autoattribute:: BaseLib.glyph .. autoattribute:: BaseLib.font Dictionary ========== .. automethod:: BaseLib.__len__ .. automethod:: BaseLib.keys .. automethod:: BaseLib.items .. automethod:: BaseLib.values .. automethod:: BaseLib.__contains__ .. automethod:: BaseLib.__setitem__ .. automethod:: BaseLib.__getitem__ .. automethod:: BaseLib.get .. automethod:: BaseLib.__delitem__ .. automethod:: BaseLib.pop .. automethod:: BaseLib.__iter__ .. automethod:: BaseLib.update .. automethod:: BaseLib.clear Environment =========== .. automethod:: BaseLib.naked .. automethod:: BaseLib.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/point.rst000066400000000000000000000045271477533125200276060ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ##### Point ##### *********** Description *********** :class:`Point ` represents one single point with a particular coordinate in a contour. It is used to access off-curve and on-curve points alike. Its cousin :class:`BPoint ` also provides access to incoming and outgoing bcps. :class:`Point ` is exclusively only one single point. :: glyph = CurrentGlyph() for contour in glyph: for point in contour.points: print(point) ******** Overview ******** Copy ==== .. autosummary:: :nosignatures: BasePoint.copy Parents ======= .. autosummary:: :nosignatures: BasePoint.contour BasePoint.glyph BasePoint.layer BasePoint.font Identification ============== .. autosummary:: :nosignatures: BasePoint.name BasePoint.identifier BasePoint.index Coordinate ========== .. autosummary:: :nosignatures: BasePoint.x BasePoint.y Type ==== .. autosummary:: :nosignatures: BasePoint.type BasePoint.smooth Transformations =============== .. autosummary:: :nosignatures: BasePoint.transformBy BasePoint.moveBy BasePoint.scaleBy BasePoint.rotateBy BasePoint.skewBy Normalization ============= .. autosummary:: :nosignatures: BasePoint.round Environment =========== .. autosummary:: :nosignatures: BasePoint.naked BasePoint.changed ********* Reference ********* .. autoclass:: BasePoint Copy ==== .. automethod:: BasePoint.copy Parents ======= .. autoattribute:: BasePoint.contour .. autoattribute:: BasePoint.glyph .. autoattribute:: BasePoint.layer .. autoattribute:: BasePoint.font Identification ============== .. autoattribute:: BasePoint.name .. autoattribute:: BasePoint.identifier .. autoattribute:: BasePoint.index Coordinate ========== .. autoattribute:: BasePoint.x .. autoattribute:: BasePoint.y Type ==== .. autoattribute:: BasePoint.type .. autoattribute:: BasePoint.smooth Transformations =============== .. automethod:: BasePoint.transformBy .. automethod:: BasePoint.moveBy .. automethod:: BasePoint.scaleBy .. automethod:: BasePoint.rotateBy .. automethod:: BasePoint.skewBy Normalization ============= .. automethod:: BasePoint.round Environment =========== .. automethod:: BasePoint.naked .. automethod:: BasePoint.changed robotools-fontParts-26e8b8c/documentation/source/objectref/objects/segment.rst000066400000000000000000000040131477533125200301050ustar00rootroot00000000000000.. highlight:: python .. module:: fontParts.base ####### Segment ####### *********** Description *********** A :class:`Contour ` object is a list of segments. A :class:`Segment ` is a list of points with some special attributes and methods. ******** Overview ******** Parents ======= .. autosummary:: :nosignatures: BaseSegment.contour BaseSegment.glyph BaseSegment.layer BaseSegment.font Identification ============== .. autosummary:: :nosignatures: BaseSegment.index Attributes ========== .. autosummary:: :nosignatures: BaseSegment.type BaseSegment.smooth Points ====== .. autosummary:: :nosignatures: BaseSegment.points BaseSegment.onCurve BaseSegment.offCurve Transformations =============== .. autosummary:: :nosignatures: BaseSegment.transformBy BaseSegment.moveBy BaseSegment.scaleBy BaseSegment.rotateBy BaseSegment.skewBy Normalization ============= .. autosummary:: :nosignatures: BaseSegment.round Environment =========== .. autosummary:: :nosignatures: BaseSegment.naked BaseSegment.changed ********* Reference ********* .. autoclass:: BaseSegment Parents ======= .. autoattribute:: BaseSegment.contour .. autoattribute:: BaseSegment.glyph .. autoattribute:: BaseSegment.layer .. autoattribute:: BaseSegment.font Identification ============== .. autoattribute:: BaseSegment.index Attributes ========== .. autoattribute:: BaseSegment.type .. autoattribute:: BaseSegment.smooth Points ====== .. autoattribute:: BaseSegment.points .. autoattribute:: BaseSegment.onCurve .. autoattribute:: BaseSegment.offCurve Transformations =============== .. automethod:: BaseSegment.transformBy .. automethod:: BaseSegment.moveBy .. automethod:: BaseSegment.scaleBy .. automethod:: BaseSegment.rotateBy .. automethod:: BaseSegment.skewBy Normalization ============= .. automethod:: BaseSegment.round Environment =========== .. automethod:: BaseSegment.naked .. automethod:: BaseSegment.changed robotools-fontParts-26e8b8c/documentation/source/objectref/valuetypes/000077500000000000000000000000001477533125200264635ustar00rootroot00000000000000robotools-fontParts-26e8b8c/documentation/source/objectref/valuetypes/index.rst000066400000000000000000000027761477533125200303400ustar00rootroot00000000000000################## Common Value Types ################## FontParts scripts are built on with objects that represent fonts, glyphs, contours and so on. The objects are obtained through :ref:`fontparts-world`. .. _fontparts-objects: FontParts uses some common value types. .. toctree:: :maxdepth: 2 :hidden: valuetypes .. _type-string: String ------ Unicode (unencoded) or string. Internally everything is a unicode string. .. _type-int-float: Integer/Float ------------- Integers and floats are interchangeable in FontParts (unless the specification states that only one is allowed). .. _type-coordinate: Coordinate ---------- An immutable iterable containing two :ref:`type-int-float` representing: #. x #. y .. _type-angle: Angle ----- XXX define the angle specifications here. Direction, degrees, etc. This will always be a float. .. _type-identifier: Identifier ---------- A :ref:`type-string` following the `UFO identifier conventions `_. .. _type-color: Color ----- An immutable iterable containing four :ref:`type-int-float` representing: #. red #. green #. blue #. alpha Values are from 0 to 1.0. .. _type-transformation: Transformation Matrix --------------------- An immutable iterable defining a 2x2 transformation plus offset (aka Affine transform). The default is ``(1, 0, 0, 1, 0, 0)``. .. _type-immutable-list: Immutable List -------------- This must be an immutable, ordered iterable like a ``tuple``.robotools-fontParts-26e8b8c/pyproject.toml000066400000000000000000000003001477533125200210530ustar00rootroot00000000000000[build-system] requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] [tool.setuptools_scm] write_to = 'Lib/fontParts/_version.py' write_to_template = '__version__ = "{version}"'robotools-fontParts-26e8b8c/requirements-dev.txt000066400000000000000000000001161477533125200222040ustar00rootroot00000000000000-r requirements.txt virtualenv>=15.0 tox>=2.3 unittest2>=1.1.0 coverage>=4.5.4robotools-fontParts-26e8b8c/requirements.txt000066400000000000000000000001421477533125200214270ustar00rootroot00000000000000FontTools[ufo,lxml,unicode]==4.55.2 fontMath==0.9.4 defcon[pens]==0.10.3 booleanOperations==0.9.0 robotools-fontParts-26e8b8c/setup.cfg000066400000000000000000000002171477533125200177670ustar00rootroot00000000000000[sdist] formats = zip [metadata] license_file = LICENSE [pycodestyle] max-line-length = 500 [options] setup_requires = setuptools_scm==8.1.0robotools-fontParts-26e8b8c/setup.py000077500000000000000000000034141477533125200176650ustar00rootroot00000000000000#! /usr/bin/env python from setuptools import setup, find_packages with open('README.rst', 'r') as f: long_description = f.read() setup_params = dict( name='fontParts', description=("An API for interacting with the parts of fonts " "during the font development process."), author='Just van Rossum, Tal Leming, Erik van Blokland, Ben Kiel, others', author_email='info@robofab.com', maintainer="Just van Rossum, Tal Leming, Erik van Blokland, Ben Kiel", maintainer_email="info@robofab.com", url='http://github.com/robotools/fontParts', license="OpenSource, MIT", platforms=["Any"], long_description=long_description, package_dir={'': 'Lib'}, packages=find_packages('Lib'), include_package_data=True, use_scm_version={ "write_to": 'Lib/fontParts/_version.py', "write_to_template": '__version__ = "{version}"', }, setup_requires=['setuptools_scm'], install_requires=[ "FontTools[ufo,lxml,unicode]>=3.32.0", "fontMath>=0.4.8", "defcon[pens]>=0.6.0", "booleanOperations>=0.9.0", ], classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: Other Environment", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Software Development :: Libraries", ], python_requires='>=3.8', zip_safe=True, ) if __name__ == "__main__": setup(**setup_params) robotools-fontParts-26e8b8c/tox.ini000066400000000000000000000015721477533125200174660ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] minversion = 3.0 envlist = py3{8,9,10,11}-cov, htmlcov [testenv] deps = cov: coverage>=4.3 -rrequirements.txt install_command = pip install -v {opts} {packages} commands = cov: coverage run --parallel-mode Lib/fontParts/fontshell/test.py {posargs} !cov: python Lib/fontParts/fontshell/test.py {posargs} [testenv:htmlcov] deps = coverage>=4.3 skip_install = true commands = coverage combine coverage html [testenv:codecov] passenv = * basepython = {env:TOXPYTHON:python} deps = coverage>=4.3 codecov skip_install = true ignore_outcome = true commands = coverage combine codecov --env TOXENV