pax_global_header00006660000000000000000000000064134052466050014517gustar00rootroot0000000000000052 comment=663101e63ddfbe5da883fe2176f2e11c678c65f1 fontPens-0.2.4/000077500000000000000000000000001340524660500133165ustar00rootroot00000000000000fontPens-0.2.4/.codecov.yml000066400000000000000000000002131340524660500155350ustar00rootroot00000000000000comment: layout: "diff, files" behavior: default require_changes: true coverage: status: project: off patch: off fontPens-0.2.4/.coveragerc000066400000000000000000000014651340524660500154450ustar00rootroot00000000000000[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 = fontPens [paths] source = Lib/fontPens .tox/*/lib/python*/site-packages/fontPens .tox/pypy*/site-packages/fontPens [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 fontPens-0.2.4/.gitignore000066400000000000000000000004201340524660500153020ustar00rootroot00000000000000# Byte-compiled / optimized files __pycache__/ *.py[co] *$py.class .cache .eggs .tox .coverage .coverage.* .pytest_cache htmlcov # OSX Finder .DS_Store # directories created during build/install process build/ dist/ Lib/fontPens.egg-info/ # PyCharm's directory .idea/ fontPens-0.2.4/.travis.yml000066400000000000000000000065421340524660500154360ustar00rootroot00000000000000sudo: false language: python python: 3.6 # empty "env:" is needed for 'allow_failures' # https://docs.travis-ci.com/user/customizing-the-build/#Rows-that-are-Allowed-to-Fail env: matrix: fast_finish: true exclude: - python: 3.6 include: - python: 2.7 env: TOXENV=py27-cov - python: 3.6 env: - TOXENV=py36-cov - BUILD_DIST=true - python: 3.7 env: TOXENV=py37-cov dist: xenial sudo: true - python: pypy env: TOXENV=pypy-nocov - language: generic os: osx env: TOXENV=py27-cov - language: generic os: osx env: - TOXENV=py3-cov - HOMEBREW_NO_AUTO_UPDATE=1 - env: - TOXENV=py27-nocov - PYENV_VERSION='2.7.6' - PYENV_VERSION_STRING='Python 2.7.6' - PYENV_ROOT=$HOME/.travis-pyenv - TRAVIS_PYENV_VERSION='0.4.0' allow_failures: # We use fast_finish + allow_failures because OSX builds take forever # https://blog.travis-ci.com/2013-11-27-fast-finishing-builds - language: generic os: osx env: TOXENV=py27-cov - language: generic os: osx env: - TOXENV=py3-cov - HOMEBREW_NO_AUTO_UPDATE=1 cache: - pip - directories: - $HOME/.pyenv_cache before_install: - source ./.travis/before_install.sh install: - ./.travis/install.sh script: - ./.travis/run.sh after_success: - ./.travis/after_success.sh before_deploy: - ./.travis/before_deploy.sh deploy: # deploy to Github Releases on tags - provider: releases api_key: secure: Kdf6HnU4mm0UK+yd+ywpN87IcUbhQ2ShjEMwenIBnrvBd2tD1hzHWrfKJVpSnVBy8QEtOcEqdRsSL4NGq/3qgp5cAxkf8hLtNChaVACeQsmQA8SLauipFPukCtkrpNdOlSDZLNHoGcIcVVPyTEkLVUpt68BxBpX8M859pA9KZ7KqFnAQFiCH06Qi5NZNHYzFBFeEcpFZyeo+3BAMEMWNfHRfkebC7BIim4de79tCui7F3C54vcGug/CuGKGU8Pbd7nfYXwEtibCDV9uOKF+kW5m7923fAvhJILrzDe3/C9KjaqtH/HQiiTNMlJ1TK1R9rodP3UUu2fN0oPpe/bMQvJUzllHMeHRB4luuaYMriRDtcCmZwCCOFeeHo3QxK8zfjv/v1lcVivPuhJiFXvx+1mU9ktSnQ5iD9QU0Si97ctJPypLaUeZq1WoluaKDv8Yw0Lmq7mZPo+rByikcGwTX7nx/LJYGlw9gtIdXSLVpsglPilM+2jVDOo7h+8oslD/AKs1S3ujDAFOeogL3SylSyswDHr4GLQMZBf/NrPBQHJge3uMYaldwKcT8xzu3LpkyCazYvt4f+Mh3RYfmE06rk86+R3luk5NNHjIJvFm0I4j+I7PtjpQYCnx6vtkH4rR0FBuE2/T0V8BBxp2UbIZF/wztBtV89oiO6yBGOauwkTM= skip_cleanup: true file_glob: true file: "dist/*" on: tags: true repo: robofab-developers/fontPens all_branches: true condition: "$BUILD_DIST == true" # deploy to PyPI on tags - provider: pypi user: benkiel password: secure: "dQmUNMwDeq7162p3IE3M9d9BW3lfk/chaW8SY605YikW7J56bfT1CQFaOiYumdvMF/eWBrVxbQbdwdupL2sDm1dSgYKWu0ite1jePaoTkMhEKFr+XW7GogcX+LOiFa7FITscnWgV02XwijStbklQmZ2beBe/tjB5Ug/Swx6CVCsTN/j1n0+r3UjtHcsnVgN+XAhpC0+ewoyKkoKP/aalb2gGFwsRDh2SzJZ/sOICHmmjJUGDB6/vS8tGgnI1arDSpSH7KDNB1dVAfDvjK9yXFEDPkYO5vEU//vZWP9yKTqXPmDiv+SMg959UcdgCUnNPSv44/VDtqv9kNhG4t3Ye4bjV/WBnyZ3SiWF8XHI+r3nk6x6Swjhq8ZRPk861JhDPK67kCJHdmMjOfaKy+MoTWscDqLlzxxAABSv+HkgQ3LiHKqBbvJVBbuhbmFdo8qtmZXKl4z8LlUcTHskMAioEMbueKRW/+jEDN5xm0h7c4W2mfHyrnn3Td5hWpmXZKe7ZKqbU8koBruGJpnC6miE689nO6HLpQbW8AjYy6ZOkz4HbkZCAYh0NqYe7qwgFKx8+iYy5smWyiqAS7A4D1kyFzaXV5jEe87jddDc/wMofgcKSUBOGe9eM/w4FKVRCW8jAMkjMiZKVMzHE2f9nk9q4A8bU++Da/AgIw3JKOVnWAPg=" skip_cleanup: true distributions: "sdist bdist_wheel" on: tags: true repo: robofab-developers/fontPens all_branches: true condition: "$BUILD_DIST == true" fontPens-0.2.4/.travis/000077500000000000000000000000001340524660500147045ustar00rootroot00000000000000fontPens-0.2.4/.travis/after_success.sh000077500000000000000000000002711340524660500200740ustar00rootroot00000000000000#!/bin/bash set -e set -x if [ "$TRAVIS_OS_NAME" == "osx" ]; then source .venv/bin/activate fi # upload coverage data to Codecov.io [[ ${TOXENV} == *"-cov"* ]] && tox -e codecov fontPens-0.2.4/.travis/before_deploy.sh000077500000000000000000000006701340524660500200640ustar00rootroot00000000000000#!/bin/bash set -e set -x # build sdist and wheel distribution packages in ./dist folder. # Travis runs the `before_deploy` stage before each deployment, but # we only want to build them once, as we want to use the same # files for both Github and PyPI if $(ls ./dist/fontPens*.zip > /dev/null 2>&1) && \ $(ls ./dist/fontPens*.whl > /dev/null 2>&1); then echo "Distribution packages already exists; skipping" else tox -e bdist fi fontPens-0.2.4/.travis/before_install.sh000077500000000000000000000003021340524660500202260ustar00rootroot00000000000000#!/bin/bash if [[ -n "$PYENV_VERSION" ]]; then wget https://github.com/praekeltfoundation/travis-pyenv/releases/download/${TRAVIS_PYENV_VERSION}/setup-pyenv.sh source setup-pyenv.sh fi fontPens-0.2.4/.travis/install.sh000077500000000000000000000016161340524660500167150ustar00rootroot00000000000000#!/bin/bash set -e set -x ci_requirements="pip setuptools tox" if [ "$TRAVIS_OS_NAME" == "osx" ]; then if [[ ${TOXENV} == *"py27"* ]]; then # install pip on the system python curl -O https://bootstrap.pypa.io/get-pip.py python get-pip.py --user # install virtualenv and create virtual environment python -m pip install --user virtualenv python -m virtualenv .venv/ elif [[ ${TOXENV} == *"py3"* ]]; then # install current python3 with homebrew # NOTE: the formula is now named just "python" brew install python command -v python3 python3 --version python3 -m pip install virtualenv python3 -m virtualenv .venv/ else echo "unsupported $TOXENV: "${TOXENV} exit 1 fi # activate virtual environment source .venv/bin/activate fi python -m pip install $ci_requirements fontPens-0.2.4/.travis/run.sh000077500000000000000000000001521340524660500160450ustar00rootroot00000000000000#!/bin/bash set -e set -x if [ "$TRAVIS_OS_NAME" == "osx" ]; then source .venv/bin/activate fi tox fontPens-0.2.4/LICENSE.txt000066400000000000000000000032741340524660500151470ustar00rootroot00000000000000fontPens License Agreement Copyright (c) 2005-2017, The RoboFab Developers: Erik van Blokland Tal Leming Just van Rossum All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the The RoboFab Developers nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Up to date info on fontPens: https://github.com/unified-font-object/fontPens This is the BSD license: http://www.opensource.org/licenses/BSD-3-Clause fontPens-0.2.4/Lib/000077500000000000000000000000001340524660500140245ustar00rootroot00000000000000fontPens-0.2.4/Lib/fontPens/000077500000000000000000000000001340524660500156205ustar00rootroot00000000000000fontPens-0.2.4/Lib/fontPens/__init__.py000066400000000000000000000000261340524660500177270ustar00rootroot00000000000000__version__ = "0.2.4" fontPens-0.2.4/Lib/fontPens/angledMarginPen.py000066400000000000000000000103161340524660500212260ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division import math from fontTools.misc.py23 import * from fontTools.pens.basePen import BasePen from fontPens.penTools import getCubicPoint class AngledMarginPen(BasePen): """ Pen to calculate the margins according to a slanted coordinate system. Slant angle comes from font.info.italicAngle. - this pen works on the on-curve points, and approximates the distance to curves. - results will be float. """ def __init__(self, glyphSet, width, italicAngle): BasePen.__init__(self, glyphSet) self.width = width self._angle = math.radians(90 + italicAngle) self.maxSteps = 100 self.margin = None self._left = None self._right = None self._start = None self.currentPoint = None def _getAngled(self, pt): right = (self.width + (pt[1] / math.tan(self._angle))) - pt[0] left = pt[0] - ((pt[1] / math.tan(self._angle))) if self._right is None: self._right = right else: self._right = min(self._right, right) if self._left is None: self._left = left else: self._left = min(self._left, left) self.margin = self._left, self._right def _moveTo(self, pt): self._start = self.currentPoint = pt def _addMoveTo(self): if self._start is None: return self._start = self.currentPoint = None def _lineTo(self, pt): self._addMoveTo() self._getAngled(pt) self.currentPoint = pt def _curveToOne(self, pt1, pt2, pt3): self._addMoveTo() step = 1.0 / self.maxSteps factors = range(0, self.maxSteps + 1) for i in factors: pt = getCubicPoint(i * step, self.currentPoint, pt1, pt2, pt3) self._getAngled(pt) self.currentPoint = pt3 def _qCurveToOne(self, bcp, pt): self._addMoveTo() self._getAngled(pt) self.currentPoint = pt def getAngledMargins(glyph, font): """ Convenience function, returns the angled margins for this glyph. Adjusted for font.info.italicAngle. """ pen = AngledMarginPen(font, glyph.width, font.info.italicAngle) glyph.draw(pen) return pen.margin def setAngledLeftMargin(glyph, font, value): """ Convenience function, sets the left angled margin to value. Adjusted for font.info.italicAngle. """ pen = AngledMarginPen(font, glyph.width, font.info.italicAngle) g.draw(pen) isLeft, isRight = pen.margin glyph.leftMargin += value - isLeft def setAngledRightMargin(glyph, font, value): """ Convenience function, sets the right angled margin to value. Adjusted for font.info.italicAngle. """ pen = AngledMarginPen(font, glyph.width, font.info.italicAngle) g.draw(pen) isLeft, isRight = pen.margin glyph.rightMargin += value - isRight def centerAngledMargins(glyph, font): """ Convenience function, centers the glyph on angled margins. """ pen = AngledMarginPen(font, glyph.width, font.info.italicAngle) g.draw(pen) isLeft, isRight = pen.margin setAngledLeftMargin(glyph, font, (isLeft + isRight) * .5) setAngledRightMargin(glyph, font, (isLeft + isRight) * .5) def guessItalicOffset(glyph, font): """ Guess the italic offset based on the margins of a symetric glyph. For instance H or I. """ l, r = getAngledMargins(glyph, font) return l - (l + r) * .5 # ========= # = tests = # ========= def _makeTestGlyph(): # make a simple glyph that we can test the pens with. from fontParts.fontshell import RGlyph testGlyph = RGlyph() testGlyph.name = "testGlyph" testGlyph.width = 1000 pen = testGlyph.getPen() pen.moveTo((100, 100)) pen.lineTo((900, 100)) pen.lineTo((900, 800)) pen.lineTo((100, 800)) pen.curveTo((120, 700), (120, 300), (100, 100)) pen.closePath() return testGlyph def _testAngledMarginPen(): """ >>> glyph = _makeTestGlyph() >>> pen = AngledMarginPen(dict(), width=0, italicAngle=10) >>> glyph.draw(pen) >>> pen.margin (117.63269807084649, -1041.061584566772) """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/digestPointPen.py000066400000000000000000000065411340524660500211340ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.pens.pointPen import AbstractPointPen class DigestPointPen(AbstractPointPen): """ This calculates a tuple representing the structure and values in a glyph: - including coordinates - including components """ def __init__(self, ignoreSmoothAndName=False): self._data = [] self.ignoreSmoothAndName = ignoreSmoothAndName def beginPath(self, identifier=None): self._data.append(('beginPath', identifier)) def endPath(self): self._data.append('endPath') def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): if self.ignoreSmoothAndName: self._data.append((pt, segmentType)) else: self._data.append((pt, segmentType, smooth, name)) def addComponent(self, baseGlyphName, transformation, identifier=None): t = [] for v in transformation: if int(v) == v: t.append(int(v)) else: t.append(v) self._data.append((baseGlyphName, tuple(t), identifier)) def getDigest(self): """ Return the digest as a tuple with all coordinates of all points. """ return tuple(self._data) def getDigestPointsOnly(self, needSort=True): """ Return the digest as a tuple with all coordinates of all points, - but without smooth info or drawing instructions. - For instance if you want to compare 2 glyphs in shape, but not interpolatability. """ points = [] for item in self._data: if isinstance(item, tuple) and isinstance(item[0], tuple): points.append(item[0]) if needSort: points.sort() return tuple(points) class DigestPointStructurePen(DigestPointPen): """ This calculates a tuple representing the structure and values in a glyph: - excluding coordinates - excluding components """ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): self._data.append(segmentType) def addComponent(self, baseGlyphName, transformation, identifier=None): self._data.append(baseGlyphName) def _testDigestPointPen(): """ >>> pen = DigestPointPen() >>> pen.beginPath("abc123") >>> pen.getDigest() (('beginPath', 'abc123'),) >>> pen.addPoint((10, 10), "move", True) >>> pen.addPoint((-10, 100), "line", False) >>> pen.endPath() >>> pen.getDigest() (('beginPath', 'abc123'), ((10, 10), 'move', True, None), ((-10, 100), 'line', False, None), 'endPath') >>> pen.getDigestPointsOnly() # https://github.com/robofab-developers/fontPens/issues/8 ((-10, 100), (10, 10)) >>> pen.beginPath() >>> pen.addPoint((100, 100), 'line') >>> pen.addPoint((100, 10), 'line') >>> pen.addPoint((10, 10), 'line') >>> pen.endPath() >>> pen.getDigest() (('beginPath', 'abc123'), ((10, 10), 'move', True, None), ((-10, 100), 'line', False, None), 'endPath', ('beginPath', None), ((100, 100), 'line', False, None), ((100, 10), 'line', False, None), ((10, 10), 'line', False, None), 'endPath') >>> pen.getDigestPointsOnly() ((-10, 100), (10, 10), (10, 10), (100, 10), (100, 100)) """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/flattenPen.py000066400000000000000000000224321340524660500202750ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.misc.bezierTools import calcQuadraticArcLength from fontTools.pens.basePen import BasePen from fontPens.penTools import estimateCubicCurveLength, distance, interpolatePoint, getCubicPoint, getQuadraticPoint class FlattenPen(BasePen): """ This filter pen processes the contours into a series of straight lines by flattening the curves. - otherPen: a different segment pen object this filter should draw the results with. - approximateSegmentLength: the length you want the flattened segments to be (roughly). - segmentLines: whether to cut straight lines into segments as well. - filterDoubles: don't draw if a segment goes to the same coordinate. """ def __init__(self, otherPen, approximateSegmentLength=5, segmentLines=False, filterDoubles=True): self.approximateSegmentLength = approximateSegmentLength BasePen.__init__(self, {}) self.otherPen = otherPen self.currentPt = None self.firstPt = None self.segmentLines = segmentLines self.filterDoubles = filterDoubles def _moveTo(self, pt): self.otherPen.moveTo(pt) self.currentPt = pt self.firstPt = pt def _lineTo(self, pt): if self.filterDoubles: if pt == self.currentPt: return if not self.segmentLines: self.otherPen.lineTo(pt) self.currentPt = pt return d = distance(self.currentPt, pt) maxSteps = int(round(d / self.approximateSegmentLength)) if maxSteps < 1: self.otherPen.lineTo(pt) self.currentPt = pt return step = 1.0 / maxSteps for factor in range(1, maxSteps + 1): self.otherPen.lineTo(interpolatePoint(self.currentPt, pt, factor * step)) self.currentPt = pt def _curveToOne(self, pt1, pt2, pt3): falseCurve = (pt1 == self.currentPt) and (pt2 == pt3) if falseCurve: self._lineTo(pt3) return est = estimateCubicCurveLength(self.currentPt, pt1, pt2, pt3) / self.approximateSegmentLength maxSteps = int(round(est)) if maxSteps < 1: self.otherPen.lineTo(pt3) self.currentPt = pt3 return step = 1.0 / maxSteps for factor in range(1, maxSteps + 1): pt = getCubicPoint(factor * step, self.currentPt, pt1, pt2, pt3) self.otherPen.lineTo(pt) self.currentPt = pt3 def _qCurveToOne(self, pt1, pt2): falseCurve = (pt1 == self.currentPt) or (pt1 == pt2) if falseCurve: self._lineTo(pt2) return est = calcQuadraticArcLength(self.currentPt, pt1, pt2) / self.approximateSegmentLength maxSteps = int(round(est)) if maxSteps < 1: self.otherPen.lineTo(pt2) self.currentPt = pt2 return step = 1.0 / maxSteps for factor in range(1, maxSteps + 1): pt = getQuadraticPoint(factor * step, self.currentPt, pt1, pt2) self.otherPen.lineTo(pt) self.currentPt = pt2 def _closePath(self): self.lineTo(self.firstPt) self.otherPen.closePath() self.currentPt = None def _endPath(self): self.otherPen.endPath() self.currentPt = None def addComponent(self, glyphName, transformation): self.otherPen.addComponent(glyphName, transformation) def flattenGlyph(aGlyph, threshold=10, segmentLines=True): """ Convenience function that applies the **FlattenPen** pen to a glyph in place. """ if len(aGlyph) == 0: return aGlyph from fontTools.pens.recordingPen import RecordingPen recorder = RecordingPen() filterpen = FlattenPen(recorder, approximateSegmentLength=threshold, segmentLines=segmentLines) aGlyph.draw(filterpen) aGlyph.clear() recorder.replay(aGlyph.getPen()) return aGlyph class SamplingPen(BasePen): """ This filter pen processes the contours into a series of straight lines by flattening the curves. Unlike FlattenPen, SamplingPen draws each curve with the given number of steps. - otherPen: a different segment pen object this filter should draw the results with. - steps: the number of steps for each curve segment. - filterDoubles: don't draw if a segment goes to the same coordinate. """ def __init__(self, otherPen, steps=10, filterDoubles=True): BasePen.__init__(self, {}) self.otherPen = otherPen self.currentPt = None self.firstPt = None self.steps = steps self.filterDoubles = filterDoubles def _moveTo(self, pt): self.otherPen.moveTo(pt) self.currentPt = pt self.firstPt = pt def _lineTo(self, pt): if self.filterDoubles: if pt == self.currentPt: return self.otherPen.lineTo(pt) self.currentPt = pt return def _curveToOne(self, pt1, pt2, pt3): falseCurve = (pt1 == self.currentPt) and (pt2 == pt3) if falseCurve: self._lineTo(pt3) return step = 1.0 / self.steps for factor in range(1, self.steps + 1): pt = getCubicPoint(factor * step, self.currentPt, pt1, pt2, pt3) self.otherPen.lineTo(pt) self.currentPt = pt3 def _qCurveToOne(self, pt1, pt2): falseCurve = (pt1 == self.currentPt) or (pt1 == pt2) if falseCurve: self._lineTo(pt2) return step = 1.0 / self.steps for factor in range(1, self.steps + 1): pt = getQuadraticPoint(factor * step, self.currentPt, pt1, pt2) self.otherPen.lineTo(pt) self.currentPt = pt2 def _closePath(self): self.lineTo(self.firstPt) self.otherPen.closePath() self.currentPt = None def _endPath(self): self.otherPen.endPath() self.currentPt = None def addComponent(self, glyphName, transformation): self.otherPen.addComponent(glyphName, transformation) def samplingGlyph(aGlyph, steps=10): """ Convenience function that applies the **SamplingPen** pen to a glyph in place. """ if len(aGlyph) == 0: return aGlyph from fontTools.pens.recordingPen import RecordingPen recorder = RecordingPen() filterpen = SamplingPen(recorder, steps=steps) aGlyph.draw(filterpen) aGlyph.clear() recorder.replay(aGlyph.getPen()) return aGlyph # ========= # = tests = # ========= def _makeTestGlyph(): # make a simple glyph that we can test the pens with. from fontParts.fontshell import RGlyph testGlyph = RGlyph() testGlyph.name = "testGlyph" testGlyph.width = 500 pen = testGlyph.getPen() pen.moveTo((10, 10)) pen.lineTo((10, 30)) pen.lineTo((30, 30)) pen.lineTo((30, 10)) pen.closePath() return testGlyph def _testFlattenPen(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyph() >>> pen = FlattenPen(PrintPen(), approximateSegmentLength=10, segmentLines=True) >>> glyph.draw(pen) pen.moveTo((10, 10)) pen.lineTo((10.0, 20.0)) pen.lineTo((10.0, 30.0)) pen.lineTo((20.0, 30.0)) pen.lineTo((30.0, 30.0)) pen.lineTo((30.0, 20.0)) pen.lineTo((30.0, 10.0)) pen.lineTo((20.0, 10.0)) pen.lineTo((10.0, 10.0)) pen.closePath() """ def _testFlattenGlyph(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyph() >>> flattenGlyph(glyph) #doctest: +ELLIPSIS >> glyph.draw(PrintPen()) pen.moveTo((10.0, 10.0)) pen.lineTo((10.0, 20.0)) pen.lineTo((10.0, 30.0)) pen.lineTo((20.0, 30.0)) pen.lineTo((30.0, 30.0)) pen.lineTo((30.0, 20.0)) pen.lineTo((30.0, 10.0)) pen.lineTo((20.0, 10.0)) pen.closePath() """ def _makeTestGlyphWithCurve(): # make a simple glyph that we can test the pens with. from fontParts.fontshell import RGlyph testGlyph = RGlyph() testGlyph.name = "testGlyph" testGlyph.width = 500 pen = testGlyph.getPen() pen.moveTo((84, 37)) pen.lineTo((348, 37)) pen.lineTo((348, 300)) pen.curveTo((265, 350.0), (177, 350.0), (84, 300)) pen.closePath() return testGlyph def _testFlattenPen(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyphWithCurve() >>> pen = SamplingPen(PrintPen(), steps=2) >>> glyph.draw(pen) pen.moveTo((84, 37)) pen.lineTo((348, 37)) pen.lineTo((348, 300)) pen.lineTo((219.75, 337.5)) pen.lineTo((84, 300)) pen.lineTo((84, 37)) pen.closePath() """ def _testFlattenGlyph(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyphWithCurve() >>> samplingGlyph(glyph) #doctest: +ELLIPSIS >> glyph.draw(PrintPen()) pen.moveTo((84, 37)) pen.lineTo((348, 37)) pen.lineTo((348, 300)) pen.lineTo((322.95, 313.5)) pen.lineTo((297.6, 324.0)) pen.lineTo((271.95, 331.5)) pen.lineTo((246.0, 336.0)) pen.lineTo((219.75, 337.5)) pen.lineTo((193.19999999999996, 336.0)) pen.lineTo((166.35, 331.5)) pen.lineTo((139.2, 324.0)) pen.lineTo((111.75, 313.5)) pen.lineTo((84, 300)) pen.closePath() """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/guessSmoothPointPen.py000066400000000000000000000075661340524660500222050ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division import math from fontTools.pens.pointPen import AbstractPointPen class GuessSmoothPointPen(AbstractPointPen): """ Filtering PointPen that tries to determine whether an on-curve point should be "smooth", ie. that it's a "tangent" point or a "curve" point. """ def __init__(self, outPen, error=0.05): self._outPen = outPen self._error = error self._points = None def _flushContour(self): points = self._points nPoints = len(points) if not nPoints: return if points[0][1] == "move": # Open path. indices = range(1, nPoints - 1) elif nPoints > 1: # Closed path. To avoid having to mod the contour index, we # simply abuse Python's negative index feature, and start at -1 indices = range(-1, nPoints - 1) else: # closed path containing 1 point (!), ignore. indices = [] for i in indices: pt, segmentType, dummy, name, kwargs = points[i] if segmentType is None: continue prev = i - 1 next = i + 1 if points[prev][1] is not None and points[next][1] is not None: continue # At least one of our neighbors is an off-curve point pt = points[i][0] prevPt = points[prev][0] nextPt = points[next][0] if pt != prevPt and pt != nextPt: dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1] dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1] a1 = math.atan2(dx1, dy1) a2 = math.atan2(dx2, dy2) if abs(a1 - a2) < self._error: points[i] = pt, segmentType, True, name, kwargs for pt, segmentType, smooth, name, kwargs in points: self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) def beginPath(self, identifier=None): assert self._points is None self._points = [] self._outPen.beginPath(identifier) def endPath(self): self._flushContour() self._outPen.endPath() self._points = None def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): self._points.append((pt, segmentType, False, name, kwargs)) def addComponent(self, glyphName, transformation, identifier=None): assert self._points is None self._outPen.addComponent(glyphName, transformation, identifier) def _testGuessSmoothPointPen(): """ >>> from fontPens.printPointPen import PrintPointPen >>> pen = GuessSmoothPointPen(PrintPointPen()) >>> pen.beginPath(identifier="abc123") pen.beginPath(identifier='abc123') >>> pen.addPoint((10, 100), "move") >>> pen.addPoint((10, 200)) >>> pen.addPoint((10, 300)) >>> pen.addPoint((10, 400), "curve") >>> pen.addPoint((10, 500)) >>> pen.endPath() pen.addPoint((10, 100), segmentType='move') pen.addPoint((10, 200)) pen.addPoint((10, 300)) pen.addPoint((10, 400), segmentType='curve', smooth=True) pen.addPoint((10, 500)) pen.endPath() >>> pen.beginPath(identifier="abc123") pen.beginPath(identifier='abc123') >>> pen.addPoint((10, 100), "move") >>> pen.addPoint((10, 200)) >>> pen.addPoint((8, 300)) >>> pen.addPoint((10, 400), "curve", smooth=False) >>> pen.addPoint((10, 500)) >>> pen.endPath() pen.addPoint((10, 100), segmentType='move') pen.addPoint((10, 200)) pen.addPoint((8, 300)) pen.addPoint((10, 400), segmentType='curve', smooth=True) pen.addPoint((10, 500)) pen.endPath() >>> pen.addComponent("a", (1, 0, 0, 1, 10, 10), "xyz987") pen.addComponent('a', (1, 0, 0, 1, 10, 10), identifier='xyz987') """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/marginPen.py000066400000000000000000000131411340524660500201120ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.pens.basePen import BasePen from fontTools.misc.bezierTools import splitLine, splitCubic class MarginPen(BasePen): """ Pen to calculate the margins at a given height or width. - isHorizontal = True: slice the glyph at y=value. - isHorizontal = False: slice the glyph at x=value. Possible optimisation: Initialise the pen object with a list of points we want to measure, then draw the glyph once, but do the splitLine() math for all measure points. """ def __init__(self, glyphSet, value, isHorizontal=True): BasePen.__init__(self, glyphSet) self.value = value self.hits = {} self.filterDoubles = True self.contourIndex = None self.startPt = None self.currentPt = None self.isHorizontal = isHorizontal def _moveTo(self, pt): self.currentPt = pt self.startPt = pt if self.contourIndex is None: self.contourIndex = 0 else: self.contourIndex += 1 def _lineTo(self, pt): if self.filterDoubles: if pt == self.currentPt: return hits = splitLine(self.currentPt, pt, self.value, self.isHorizontal) if len(hits) > 1: # result will be 2 tuples of 2 coordinates # first two points: start to intersect # second two points: intersect to end # so, second point in first tuple is the intersect # then, the first coordinate of that point is the x. if self.contourIndex not in self.hits: self.hits[self.contourIndex] = [] if self.isHorizontal: self.hits[self.contourIndex].append(round(hits[0][-1][0], 4)) else: self.hits[self.contourIndex].append(round(hits[0][-1][1], 4)) if self.isHorizontal and pt[1] == self.value: # it could happen if self.contourIndex not in self.hits: self.hits[self.contourIndex] = [] self.hits[self.contourIndex].append(pt[0]) elif (not self.isHorizontal) and (pt[0] == self.value): # it could happen if self.contourIndex not in self.hits: self.hits[self.contourIndex] = [] self.hits[self.contourIndex].append(pt[1]) self.currentPt = pt def _curveToOne(self, pt1, pt2, pt3): hits = splitCubic(self.currentPt, pt1, pt2, pt3, self.value, self.isHorizontal) for i in range(len(hits) - 1): # a number of intersections is possible. Just take the # last point of each segment. if self.contourIndex not in self.hits: self.hits[self.contourIndex] = [] if self.isHorizontal: self.hits[self.contourIndex].append(round(hits[i][-1][0], 4)) else: self.hits[self.contourIndex].append(round(hits[i][-1][1], 4)) if self.isHorizontal and pt3[1] == self.value: # it could happen if self.contourIndex not in self.hits: self.hits[self.contourIndex] = [] self.hits[self.contourIndex].append(pt3[0]) if (not self.isHorizontal) and (pt3[0] == self.value): # it could happen if self.contourIndex not in self.hits: self.hits[self.contourIndex] = [] self.hits[self.contourIndex].append(pt3[1]) self.currentPt = pt3 def _closePath(self): if self.currentPt != self.startPt: self._lineTo(self.startPt) self.currentPt = self.startPt = None def _endPath(self): self.currentPt = None def addComponent(self, baseGlyph, transformation): if self.glyphSet is None: return if baseGlyph in self.glyphSet: glyph = self.glyphSet[baseGlyph] if glyph is not None: glyph.draw(self) def getMargins(self): """ Return the extremes of the slice for all contours combined, i.e. the whole glyph. """ allHits = [] for index, pts in self.hits.items(): allHits.extend(pts) if allHits: return min(allHits), max(allHits) return None def getContourMargins(self): """ Return the extremes of the slice for each contour. """ allHits = {} for index, pts in self.hits.items(): unique = list(set(pts)) unique.sort() allHits[index] = unique return allHits def getAll(self): """ Return all the slices. """ allHits = [] for index, pts in self.hits.items(): allHits.extend(pts) unique = list(set(allHits)) unique.sort() return unique # ========= # = tests = # ========= def _makeTestGlyph(): # make a simple glyph that we can test the pens with. from fontParts.fontshell import RGlyph testGlyph = RGlyph() testGlyph.name = "testGlyph" testGlyph.width = 1000 pen = testGlyph.getPen() pen.moveTo((100, 100)) pen.lineTo((900, 100)) pen.lineTo((900, 800)) pen.lineTo((100, 800)) # a curve pen.curveTo((120, 700), (120, 300), (100, 100)) pen.closePath() # pen.addComponent("a", (1, 0, 0, 1, 0, 0)) return testGlyph def _testMarginPen(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyph() >>> pen = MarginPen(dict(), 200, isHorizontal=True) >>> glyph.draw(pen) >>> pen.getAll() [107.5475, 900.0] """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/penTools.py000066400000000000000000000141631340524660500200020ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division import math from fontTools.misc.py23 import * from fontTools.misc.bezierTools import calcQuadraticArcLengthC def distance(pt1, pt2): """ The distance between two points >>> distance((0, 0), (10, 0)) 10.0 >>> distance((0, 0), (-10, 0)) 10.0 >>> distance((0, 0), (0, 10)) 10.0 >>> distance((0, 0), (0, -10)) 10.0 >>> distance((10, 10), (13, 14)) 5.0 """ return math.sqrt((pt1[0] - pt2[0])**2 + (pt1[1] - pt2[1])**2) def middlePoint(pt1, pt2): """ Return the point that lies in between the two input points. """ (x0, y0), (x1, y1) = pt1, pt2 return 0.5 * (x0 + x1), 0.5 * (y0 + y1) def getCubicPoint(t, pt0, pt1, pt2, pt3): """ Return the point for t on the cubic curve defined by pt0, pt1, pt2, pt3. >>> getCubicPoint(0.00, (0, 0), (50, -10), (80, 50), (120, 40)) (0, 0) >>> getCubicPoint(0.20, (0, 0), (50, -10), (80, 50), (120, 40)) (27.84, 1.280000000000002) >>> getCubicPoint(0.50, (0, 0), (50, -10), (80, 50), (120, 40)) (63.75, 20.0) >>> getCubicPoint(0.85, (0, 0), (50, -10), (80, 50), (120, 40)) (102.57375, 40.2475) >>> getCubicPoint(1.00, (0, 0), (50, -10), (80, 50), (120, 40)) (120, 40) """ if t == 0: return pt0 if t == 1: return pt3 if t == 0.5: a = middlePoint(pt0, pt1) b = middlePoint(pt1, pt2) c = middlePoint(pt2, pt3) d = middlePoint(a, b) e = middlePoint(b, c) return middlePoint(d, e) else: cx = (pt1[0] - pt0[0]) * 3 cy = (pt1[1] - pt0[1]) * 3 bx = (pt2[0] - pt1[0]) * 3 - cx by = (pt2[1] - pt1[1]) * 3 - cy ax = pt3[0] - pt0[0] - cx - bx ay = pt3[1] - pt0[1] - cy - by t3 = t ** 3 t2 = t * t x = ax * t3 + bx * t2 + cx * t + pt0[0] y = ay * t3 + by * t2 + cy * t + pt0[1] return x, y def getQuadraticPoint(t, pt0, pt1, pt2): """ Return the point for t on the quadratic curve defined by pt0, pt1, pt2, pt3. >>> getQuadraticPoint(0.00, (0, 0), (50, -10), (80, 50)) (0, 0) >>> getQuadraticPoint(0.21, (0, 0), (50, -10), (80, 50)) (20.118, -1.113) >>> getQuadraticPoint(0.50, (0, 0), (50, -10), (80, 50)) (45.0, 7.5) >>> getQuadraticPoint(0.87, (0, 0), (50, -10), (80, 50)) (71.862, 35.583) >>> getQuadraticPoint(1.00, (0, 0), (50, -10), (80, 50)) (80, 50) """ if t == 0: return pt0 if t == 1: return pt2 a = (1 - t) ** 2 b = 2 * (1 - t) * t c = t ** 2 x = a * pt0[0] + b * pt1[0] + c * pt2[0]; y = a * pt0[1] + b * pt1[1] + c * pt2[1]; return x, y def getCubicPoints(ts, pt0, pt1, pt2, pt3): """ Return a list of points for increments of t on the cubic curve defined by pt0, pt1, pt2, pt3. >>> getCubicPoints([i/10 for i in range(11)], (0, 0), (50, -10), (80, 50), (120, 40)) [(0.0, 0.0), (14.43, -1.0399999999999996), (27.84, 1.280000000000002), (40.41, 6.119999999999999), (52.32, 12.640000000000008), (63.75, 20.0), (74.88, 27.36), (85.89, 33.88), (96.96, 38.72000000000001), (108.27000000000001, 41.040000000000006), (120.0, 40.0)] """ (x0, y0), (x1, y1) = pt0, pt1 cx = (x1 - x0) * 3 cy = (y1 - y0) * 3 bx = (pt2[0] - x1) * 3 - cx by = (pt2[1] - y1) * 3 - cy ax = pt3[0] - x0 - cx - bx ay = pt3[1] - y0 - cy - by path = [] for t in ts: t3 = t ** 3 t2 = t * t x = ax * t3 + bx * t2 + cx * t + x0 y = ay * t3 + by * t2 + cy * t + y0 path.append((x, y)) return path def estimateCubicCurveLength(pt0, pt1, pt2, pt3, precision=10): """ Estimate the length of this curve by iterating through it and averaging the length of the flat bits. >>> estimateCubicCurveLength((0, 0), (0, 0), (0, 0), (0, 0), 1000) 0.0 >>> estimateCubicCurveLength((0, 0), (0, 0), (120, 0), (120, 0), 1000) 120.0 >>> estimateCubicCurveLength((0, 0), (50, 0), (80, 0), (120, 0), 1000) 120.0 >>> estimateCubicCurveLength((0, 0), (50, -10), (80, 50), (120, 40), 1) 126.49110640673517 >>> estimateCubicCurveLength((0, 0), (50, -10), (80, 50), (120, 40), 1000) 130.4488917899906 """ length = 0 step = 1.0 / precision points = getCubicPoints([f * step for f in range(precision + 1)], pt0, pt1, pt2, pt3) for i in range(len(points) - 1): pta = points[i] ptb = points[i + 1] length += distance(pta, ptb) return length def estimateQuadraticCurveLength(pt0, pt1, pt2, precision=10): """ Estimate the length of this curve by iterating through it and averaging the length of the flat bits. >>> estimateQuadraticCurveLength((0, 0), (0, 0), (0, 0)) # empty segment 0.0 >>> estimateQuadraticCurveLength((0, 0), (50, 0), (80, 0)) # collinear points 80.0 >>> estimateQuadraticCurveLength((0, 0), (50, 20), (100, 40)) # collinear points 107.70329614269009 >>> estimateQuadraticCurveLength((0, 0), (0, 100), (100, 0)) 153.6861437729263 >>> estimateQuadraticCurveLength((0, 0), (50, -10), (80, 50)) 102.4601733446439 >>> estimateQuadraticCurveLength((0, 0), (40, 0), (-40, 0)) # collinear points, control point outside 66.39999999999999 >>> estimateQuadraticCurveLength((0, 0), (40, 0), (0, 0)) # collinear points, looping back 40.0 """ points = [] length = 0 step = 1.0 / precision factors = range(0, precision + 1) for i in factors: points.append(getQuadraticPoint(i * step, pt0, pt1, pt2)) for i in range(len(points) - 1): pta = points[i] ptb = points[i + 1] length += distance(pta, ptb) return length def interpolatePoint(pt1, pt2, v): """ interpolate point by factor v >>> interpolatePoint((0, 0), (10, 10), 0.5) (5.0, 5.0) >>> interpolatePoint((0, 0), (10, 10), 0.6) (6.0, 6.0) """ (xa, ya), (xb, yb) = pt1, pt2 if not isinstance(v, tuple): xv = v yv = v else: xv, yv = v return xa + (xb - xa) * xv, ya + (yb - ya) * yv if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/printPen.py000066400000000000000000000050421340524660500177720ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.misc.py23 import * from fontTools.pens.basePen import AbstractPen class PrintPen(AbstractPen): """ A SegmentPen that prints every step. """ def moveTo(self, pt): print("pen.moveTo(%s)" % (tuple(pt),)) def lineTo(self, pt): print("pen.lineTo(%s)" % (tuple(pt),)) def curveTo(self, *pts): args = self._pointArgsRepr(pts) print("pen.curveTo(%s)" % args) def qCurveTo(self, *pts): args = self._pointArgsRepr(pts) print("pen.qCurveTo(%s)" % args) def closePath(self): print("pen.closePath()") def endPath(self): print("pen.endPath()") def addComponent(self, baseGlyphName, transformation): print("pen.addComponent('%s', %s)" % (baseGlyphName, tuple(transformation))) @staticmethod def _pointArgsRepr(pts): return ", ".join("None" if pt is None else str(tuple(pt)) for pt in pts) def _testPrintPen(): """ >>> pen = PrintPen() >>> pen.moveTo((10, 10)) pen.moveTo((10, 10)) >>> pen.lineTo((20, 20)) pen.lineTo((20, 20)) >>> pen.curveTo((1, 1), (2, 2), (3, 3)) pen.curveTo((1, 1), (2, 2), (3, 3)) >>> pen.qCurveTo((4, 4), (5, 5)) pen.qCurveTo((4, 4), (5, 5)) >>> pen.qCurveTo((6, 6)) pen.qCurveTo((6, 6)) >>> pen.closePath() pen.closePath() >>> pen.endPath() pen.endPath() >>> pen.addComponent("a", (1, 0, 0, 1, 10, 10)) pen.addComponent('a', (1, 0, 0, 1, 10, 10)) >>> pen.curveTo((1, 1), (2, 2), (3, 3), None) pen.curveTo((1, 1), (2, 2), (3, 3), None) >>> pen.qCurveTo((1, 1), (2, 2), (3, 3), None) pen.qCurveTo((1, 1), (2, 2), (3, 3), None) """ def _testPrintPen_nonTuplePoints(): """ >>> pen = PrintPen() >>> pen.moveTo([10, 10]) pen.moveTo((10, 10)) >>> pen.lineTo([20, 20]) pen.lineTo((20, 20)) >>> pen.curveTo([1, 1], [2, 2], [3, 3]) pen.curveTo((1, 1), (2, 2), (3, 3)) >>> pen.qCurveTo([4, 4], [5, 5]) pen.qCurveTo((4, 4), (5, 5)) >>> pen.qCurveTo([6, 6]) pen.qCurveTo((6, 6)) >>> pen.closePath() pen.closePath() >>> pen.endPath() pen.endPath() >>> pen.addComponent("a", [1, 0, 0, 1, 10, 10]) pen.addComponent('a', (1, 0, 0, 1, 10, 10)) >>> pen.curveTo([1, 1], [2, 2], [3, 3], None) pen.curveTo((1, 1), (2, 2), (3, 3), None) >>> pen.qCurveTo([1, 1], [2, 2], [3, 3], None) pen.qCurveTo((1, 1), (2, 2), (3, 3), None) """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/printPointPen.py000066400000000000000000000046141340524660500210100ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.misc.py23 import * from fontTools.pens.pointPen import AbstractPointPen class PrintPointPen(AbstractPointPen): """ A PointPen that prints every step. """ def __init__(self): self.havePath = False def beginPath(self, identifier=None): self.havePath = True if identifier is not None: print("pen.beginPath(identifier=%r)" % identifier) else: print("pen.beginPath()") def endPath(self): self.havePath = False print("pen.endPath()") def addPoint(self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): assert self.havePath args = ["(%s, %s)" % (pt[0], pt[1])] if segmentType is not None: args.append("segmentType='%s'" % segmentType) if smooth: args.append("smooth=True") if name is not None: args.append("name='%s'" % name) if identifier is not None: args.append("identifier='%s'" % identifier) if kwargs: args.append("**%s" % kwargs) print("pen.addPoint(%s)" % ", ".join(args)) def addComponent(self, baseGlyphName, transformation, identifier=None): assert not self.havePath args = "'%s', %r" % (baseGlyphName, transformation) if identifier is not None: args += ", identifier='%s'" % identifier print("pen.addComponent(%s)" % args) def _testPrintPointPen(): """ >>> pen = PrintPointPen() >>> pen.beginPath() pen.beginPath() >>> pen.beginPath("abc123") pen.beginPath(identifier='abc123') >>> pen.addPoint((10, 10), "curve", True, identifier="abc123") pen.addPoint((10, 10), segmentType='curve', smooth=True, identifier='abc123') >>> pen.addPoint((10, 10), "curve", True) pen.addPoint((10, 10), segmentType='curve', smooth=True) >>> pen.endPath() pen.endPath() >>> pen.addComponent("a", (1, 0, 0, 1, 10, 10), "xyz987") pen.addComponent('a', (1, 0, 0, 1, 10, 10), identifier='xyz987') >>> pen.addComponent("a", (1, 0, 0, 1, 10, 10), identifier="xyz9876") pen.addComponent('a', (1, 0, 0, 1, 10, 10), identifier='xyz9876') >>> pen.addComponent("a", (1, 0, 0, 1, 10, 10)) pen.addComponent('a', (1, 0, 0, 1, 10, 10)) """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/recordingPointPen.py000066400000000000000000000042441340524660500216270ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.pens.pointPen import AbstractPointPen def replayRecording(recording, pen): """Replay a recording, as produced by RecordingPointPen, to a pointpen. Note that recording does not have to be produced by those pens. It can be any iterable of tuples of method name and tuple-of-arguments. Likewise, pen can be any objects receiving those method calls. """ for operator, operands, kwargs in recording: getattr(pen, operator)(*operands, **kwargs) class RecordingPointPen(AbstractPointPen): def __init__(self): self.value = [] def beginPath(self, **kwargs): self.value.append(("beginPath", (), kwargs)) def endPath(self): self.value.append(("endPath", (), {})) def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs)) def addComponent(self, baseGlyphName, transformation, **kwargs): self.value.append(("addComponent", (baseGlyphName, transformation), kwargs)) def replay(self, pen): replayRecording(self.value, pen) def _test(): """ >>> from fontPens.printPointPen import PrintPointPen >>> pen = RecordingPointPen() >>> pen.beginPath() >>> pen.addPoint((100, 200), smooth=False, segmentType="line") >>> pen.endPath() >>> pen.beginPath(identifier="my_path_id") >>> pen.addPoint((200, 300), segmentType="line") >>> pen.addPoint((200, 400), segmentType="line", identifier="my_point_id") >>> pen.endPath() >>> pen2 = RecordingPointPen() >>> pen.replay(pen2) >>> assert pen.value == pen2.value >>> ppp = PrintPointPen() >>> pen2.replay(ppp) pen.beginPath() pen.addPoint((100, 200), segmentType='line') pen.endPath() pen.beginPath(identifier='my_path_id') pen.addPoint((200, 300), segmentType='line') pen.addPoint((200, 400), segmentType='line', identifier='my_point_id') pen.endPath() """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/thresholdPen.py000066400000000000000000000060741340524660500206400ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.pens.basePen import AbstractPen from fontPens.penTools import distance class ThresholdPen(AbstractPen): """ This pen only draws segments longer in length than the threshold value. - otherPen: a different segment pen object this filter should draw the results with. - threshold: the minimum length of a segment """ def __init__(self, otherPen, threshold=10): self.threshold = threshold self._lastPt = None self.otherPen = otherPen def moveTo(self, pt): self._lastPt = pt self.otherPen.moveTo(pt) def lineTo(self, pt, smooth=False): if self.threshold <= distance(pt, self._lastPt): self.otherPen.lineTo(pt) self._lastPt = pt def curveTo(self, pt1, pt2, pt3): if self.threshold <= distance(pt3, self._lastPt): self.otherPen.curveTo(pt1, pt2, pt3) self._lastPt = pt3 def qCurveTo(self, *points): if self.threshold <= distance(points[-1], self._lastPt): self.otherPen.qCurveTo(*points) self._lastPt = points[-1] def closePath(self): self.otherPen.closePath() def endPath(self): self.otherPen.endPath() def addComponent(self, glyphName, transformation): self.otherPen.addComponent(glyphName, transformation) def thresholdGlyph(aGlyph, threshold=10): """ Convenience function that applies the **ThresholdPen** to a glyph in place. """ from fontTools.pens.recordingPen import RecordingPen recorder = RecordingPen() filterpen = ThresholdPen(recorder, threshold) aGlyph.draw(filterpen) aGlyph.clear() recorder.replay(aGlyph.getPen()) return aGlyph # ========= # = tests = # ========= def _makeTestGlyph(): # make a simple glyph that we can test the pens with. from fontParts.fontshell import RGlyph testGlyph = RGlyph() testGlyph.name = "testGlyph" testGlyph.width = 1000 pen = testGlyph.getPen() pen.moveTo((100, 100)) pen.lineTo((900, 100)) pen.lineTo((900, 109)) pen.lineTo((900, 800)) pen.lineTo((100, 800)) pen.closePath() pen.addComponent("a", (1, 0, 0, 1, 0, 0)) return testGlyph def _testThresholdPen(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyph() >>> pen = ThresholdPen(PrintPen()) >>> glyph.draw(pen) pen.moveTo((100, 100)) pen.lineTo((900, 100)) pen.lineTo((900, 800)) pen.lineTo((100, 800)) pen.closePath() pen.addComponent('a', (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)) """ def _testThresholdGlyph(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyph() >>> thresholdGlyph(glyph) #doctest: +ELLIPSIS >> glyph.draw(PrintPen()) pen.moveTo((100, 100)) pen.lineTo((900, 100)) pen.lineTo((900, 800)) pen.lineTo((100, 800)) pen.closePath() pen.addComponent('a', (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)) """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/thresholdPointPen.py000066400000000000000000000076231340524660500216530ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.pens.pointPen import AbstractPointPen from fontPens.penTools import distance class ThresholdPointPen(AbstractPointPen): """ Rewrite of the ThresholdPen as a PointPen so that we can preserve named points and other arguments. This pen will add components from the original glyph, but but it won't filter those components. "move", "line", "curve" or "qcurve" """ def __init__(self, otherPointPen, threshold=10): self.threshold = threshold self._lastPt = None self._offCurveBuffer = [] self.otherPointPen = otherPointPen def beginPath(self, identifier=None): """Start a new sub path.""" self.otherPointPen.beginPath(identifier) self._lastPt = None def endPath(self): """End the current sub path.""" self.otherPointPen.endPath() def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): """Add a point to the current sub path.""" if segmentType in ['curve', 'qcurve']: # it's an offcurve, let's buffer them until we get another oncurve # and we know what to do with them self._offCurveBuffer.append((pt, segmentType, smooth, name, kwargs)) return elif segmentType == "move": # start of an open contour self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs? self._lastPt = pt self._offCurveBuffer = [] elif segmentType == "line": if self._lastPt is None: self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs? self._lastPt = pt elif distance(pt, self._lastPt) >= self.threshold: # we're oncurve and far enough from the last oncurve if self._offCurveBuffer: # empty any buffered offcurves for buf_pt, buf_segmentType, buf_smooth, buf_name, buf_kwargs in self._offCurveBuffer: self.otherPointPen.addPoint(buf_pt, buf_segmentType, buf_smooth, buf_name) # how to add kwargs? self._offCurveBuffer = [] # finally add the oncurve. self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs? self._lastPt = pt else: # we're too short, so we're not going to make it. # we need to clear out the offcurve buffer. self._offCurveBuffer = [] def addComponent(self, baseGlyphName, transformation, identifier=None): """Add a sub glyph. Note: this way components are not filtered.""" self.otherPointPen.addComponent(baseGlyphName, transformation, identifier) # ========= # = tests = # ========= def _makeTestGlyph(): # make a simple glyph that we can test the pens with. from fontParts.fontshell import RGlyph testGlyph = RGlyph() testGlyph.name = "testGlyph" testGlyph.width = 1000 pen = testGlyph.getPen() pen.moveTo((100, 100)) pen.lineTo((900, 100)) pen.lineTo((900, 109)) pen.lineTo((900, 800)) pen.lineTo((100, 800)) pen.closePath() pen.addComponent("a", (1, 0, 0, 1, 0, 0)) return testGlyph def _testThresholdPen(): """ >>> from fontPens.printPointPen import PrintPointPen >>> from random import seed >>> seed(100) >>> glyph = _makeTestGlyph() >>> pen = ThresholdPointPen(PrintPointPen()) >>> glyph.drawPoints(pen) pen.beginPath() pen.addPoint((100, 100), segmentType='line') pen.addPoint((900, 100), segmentType='line') pen.addPoint((900, 800), segmentType='line') pen.addPoint((100, 800), segmentType='line') pen.endPath() pen.addComponent('a', (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)) """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/Lib/fontPens/transformPointPen.py000066400000000000000000000032661340524660500216710ustar00rootroot00000000000000from __future__ import absolute_import, print_function, division from fontTools.pens.pointPen import AbstractPointPen class TransformPointPen(AbstractPointPen): """ PointPen that transforms all coordinates, and passes them to another PointPen. It also transforms the transformation given to addComponent(). """ def __init__(self, outPen, transformation): if not hasattr(transformation, "transformPoint"): from fontTools.misc.transform import Transform transformation = Transform(*transformation) self._transformation = transformation self._transformPoint = transformation.transformPoint self._outPen = outPen self._stack = [] def beginPath(self, identifier=None): self._outPen.beginPath(identifier=identifier) def endPath(self): self._outPen.endPath() def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): pt = self._transformPoint(pt) self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) def addComponent(self, glyphName, transformation, identifier=None): transformation = self._transformation.transform(transformation) self._outPen.addComponent(glyphName, transformation, identifier) def _testTransformPointPen(): """ >>> from fontPens.printPointPen import PrintPointPen >>> pen = TransformPointPen(PrintPointPen(), (1, 0, 0, 1, 20, 20)) >>> pen.beginPath() pen.beginPath() >>> pen.addPoint((0, 0), "move", name="hello") pen.addPoint((20, 20), segmentType='move', name='hello') >>> pen.endPath() pen.endPath() """ if __name__ == "__main__": import doctest doctest.testmod() fontPens-0.2.4/MANIFEST.in000066400000000000000000000000471340524660500150550ustar00rootroot00000000000000include README.rst include LICENSE.txt fontPens-0.2.4/README.rst000066400000000000000000000012451340524660500150070ustar00rootroot00000000000000|Build Status| |Coverage| |PyPI| |Versions| fontPens -------- A collection of classes implementing the pen protocol for manipulating glyphs. .. |Build Status| image:: https://travis-ci.org/robofab-developers/fontPens.svg?branch=master :target: https://travis-ci.org/robofab-developers/fontPens .. |PyPI| image:: https://img.shields.io/pypi/v/fontPens.svg :target: https://pypi.org/project/fontPens .. |Versions| image:: https://img.shields.io/badge/python-2.7%2C%203.7-blue.svg :alt: Python Versions .. |Coverage| image:: https://codecov.io/gh/robofab-developers/fontPens/branch/master/graph/badge.svg :target: https://codecov.io/gh/robofab-developers/fontPens fontPens-0.2.4/dev-requirements.txt000066400000000000000000000000461340524660500173560ustar00rootroot00000000000000pytest>=3.0 tox>=2.5 fontParts>=0.8.4 fontPens-0.2.4/requirements.txt000066400000000000000000000000221340524660500165740ustar00rootroot00000000000000FontTools>=3.32.0 fontPens-0.2.4/setup.cfg000066400000000000000000000014341340524660500151410ustar00rootroot00000000000000[bumpversion] current_version = 0.2.4 commit = True tag = False tag_name = v{new_version} parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? serialize = {major}.{minor}.{patch}.{release}{dev} {major}.{minor}.{patch} [bumpversion:part:release] optional_value = final values = dev final [bumpversion:part:dev] [bumpversion:file:Lib/fontPens/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" [bumpversion:file:setup.py] search = version="{current_version}" replace = version="{new_version}" [wheel] universal = 1 [sdist] formats = zip [aliases] test = pytest [metadata] license_file = LICENSE.txt [tool:pytest] minversion = 3.0.2 testpaths = Lib/fontPens addopts = -v -r a --doctest-modules fontPens-0.2.4/setup.py000077500000000000000000000143731340524660500150430ustar00rootroot00000000000000#! /usr/bin/env python import sys from setuptools import setup, find_packages, Command from distutils import log class bump_version(Command): description = "increment the package version and commit the changes" user_options = [ ("major", None, "bump the first digit, for incompatible API changes"), ("minor", None, "bump the second digit, for new backward-compatible features"), ("patch", None, "bump the third digit, for bug fixes (default)"), ] def initialize_options(self): self.minor = False self.major = False self.patch = False def finalize_options(self): part = None for attr in ("major", "minor", "patch"): if getattr(self, attr, False): if part is None: part = attr else: from distutils.errors import DistutilsOptionError raise DistutilsOptionError( "version part options are mutually exclusive") self.part = part or "patch" def bumpversion(self, part, **kwargs): """ Run bump2version.main() with the specified arguments. """ import bumpversion args = ['--verbose'] if self.verbose > 1 else [] for k, v in kwargs.items(): k = "--{}".format(k.replace("_", "-")) is_bool = isinstance(v, bool) and v is True args.extend([k] if is_bool else [k, str(v)]) args.append(part) log.debug( "$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args)) bumpversion.main(args) def run(self): log.info("bumping '%s' version" % self.part) self.bumpversion(self.part) class release(bump_version): """Drop the developmental release '.devN' suffix from the package version, open the default text $EDITOR to write release notes, commit the changes and generate a git tag. Release notes can also be set with the -m/--message option, or by reading from standard input. """ description = "tag a new release" user_options = [ ("message=", 'm', "message containing the release notes"), ] def initialize_options(self): self.message = None def finalize_options(self): import re current_version = self.distribution.metadata.get_version() if not re.search(r"\.dev[0-9]+", current_version): from distutils.errors import DistutilsSetupError raise DistutilsSetupError( "current version (%s) has no '.devN' suffix.\n " "Run 'setup.py bump_version' with any of " "--major, --minor, --patch options" % current_version) message = self.message if message is None: if sys.stdin.isatty(): # stdin is interactive, use editor to write release notes message = self.edit_release_notes() else: # read release notes from stdin pipe message = sys.stdin.read() if not message.strip(): from distutils.errors import DistutilsSetupError raise DistutilsSetupError("release notes message is empty") self.message = "v{new_version}\n\n%s" % message @staticmethod def edit_release_notes(): """Use the default text $EDITOR to write release notes. If $EDITOR is not set, use 'nano'.""" from tempfile import mkstemp import os import shlex import subprocess text_editor = shlex.split(os.environ.get('EDITOR', 'nano')) fd, tmp = mkstemp(prefix='bumpversion-') try: os.close(fd) with open(tmp, 'w') as f: f.write("\n\n# Write release notes.\n" "# Lines starting with '#' will be ignored.") subprocess.check_call(text_editor + [tmp]) with open(tmp, 'r') as f: changes = "".join( l for l in f.readlines() if not l.startswith('#')) finally: os.remove(tmp) return changes def run(self): log.info("stripping developmental release suffix") # drop '.dev0' suffix, commit with given message and create git tag self.bumpversion("release", tag=True, message="Release {new_version}", tag_message=self.message) needs_pytest = {'pytest', 'test'}.intersection(sys.argv) pytest_runner = ['pytest_runner'] if needs_pytest else [] needs_wheel = {'bdist_wheel'}.intersection(sys.argv) wheel = ['wheel'] if needs_wheel else [] needs_bump2version = {'release', 'bump_version'}.intersection(sys.argv) bump2version = ['bump2version'] if needs_bump2version else [] with open('README.rst', 'r') as f: long_description = f.read() setup_params = dict( name="fontPens", version="0.2.4", description=("A collection of classes implementing the pen " "protocol for manipulating glyphs."), author="Just van Rossum, Tal Leming, Erik van Blokland, others", author_email="info@robofab.com", maintainer="Just van Rossum, Tal Leming, Erik van Blokland", maintainer_email="info@robofab.com", url="https://github.com/robofab-developers/fontPens", license="OpenSource, BSD-style", platforms=["Any"], long_description=long_description, package_dir={'': 'Lib'}, packages=find_packages('Lib'), include_package_data=True, setup_requires=pytest_runner + wheel + bump2version, tests_require=[ 'pytest', 'fontParts>=0.8.1' ], install_requires=[ "FontTools>=3.32.0", ], cmdclass={ "release": release, "bump_version": bump_version, }, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: Other Environment", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics :: Graphics Conversion", ], ) if __name__ == "__main__": setup(**setup_params) fontPens-0.2.4/tox.ini000066400000000000000000000031221340524660500146270ustar00rootroot00000000000000# 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] envlist = py{27,36,37}-cov, py{py}-nocov, htmlcov [testenv] deps = cov: coverage>=4.3 pytest fontparts>=0.8.1 FontTools[ufo,lxml,unicode]>=3.32.0 install_command = pip install -v {opts} {packages} commands = cov: coverage run --parallel-mode -m pytest {posargs} nocov: pytest {posargs} [testenv:htmlcov] basepython = {env:TOXPYTHON:python3.6} 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 [testenv:bdist] basepython = {env:TOXPYTHON:python3.6} deps = pygments docutils setuptools wheel skip_install = true install_command = # make sure we use the latest setuptools and wheel pip install --upgrade {opts} {packages} whitelist_externals = rm commands = # check metadata and rst long_description python setup.py check --restructuredtext --strict # clean up build/ and dist/ folders rm -rf {toxinidir}/dist python setup.py clean --all # build sdist python setup.py sdist --dist-dir {toxinidir}/dist # build wheel from sdist pip wheel -v --no-deps --no-index --wheel-dir {toxinidir}/dist --find-links {toxinidir}/dist fontPens