pax_global_header00006660000000000000000000000064152075562670014531gustar00rootroot0000000000000052 comment=0d41a72d59965f89919053425d8fbe096f465f52 robotools-fontPens-7a4c5bc/000077500000000000000000000000001520755626700160375ustar00rootroot00000000000000robotools-fontPens-7a4c5bc/.codecov.yml000066400000000000000000000002131520755626700202560ustar00rootroot00000000000000comment: layout: "diff, files" behavior: default require_changes: true coverage: status: project: off patch: off robotools-fontPens-7a4c5bc/.coveragerc000066400000000000000000000014651520755626700201660ustar00rootroot00000000000000[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 robotools-fontPens-7a4c5bc/.flake8000066400000000000000000000001441520755626700172110ustar00rootroot00000000000000[flake8] ignore = C901,E203,W503 max-line-length = 100 max-complexity = 15 select = B,C,E,F,W,T4,B9 robotools-fontPens-7a4c5bc/.github/000077500000000000000000000000001520755626700173775ustar00rootroot00000000000000robotools-fontPens-7a4c5bc/.github/dependabot.yml000066400000000000000000000005271520755626700222330ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" open-pull-requests-limit: 10 groups: python-packages: patterns: - "*" robotools-fontPens-7a4c5bc/.github/workflows/000077500000000000000000000000001520755626700214345ustar00rootroot00000000000000robotools-fontPens-7a4c5bc/.github/workflows/ci.yml000066400000000000000000000067451520755626700225660ustar00rootroot00000000000000name: Run tests and deploy on: push: branches: [ main ] tags: [ "v*" ] pull_request: branches: [ main ] jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 - uses: pre-commit/action@v3.0.1 with: extra_args: --all-files --verbose --show-diff-on-failure test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] python-version: ["3.10", "3.14"] steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt pip install . - name: Test with pytest run: | pytest -v publish: # only run if the commit is tagged... if: startsWith(github.ref, 'refs/tags/v') # ... and the test job completed successfully needs: [pre-commit, test] runs-on: ubuntu-latest # Specifying a GitHub environment is optional, but strongly encouraged environment: publish-to-pypi permissions: # IMPORTANT: this permission is mandatory for Trusted Publishing id-token: write contents: write steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.x" - name: Install dependencies run: | python -m pip install --upgrade pip pip install --upgrade build - name: Build wheel and source distro run: | python -m build - name: Extract release notes from annotated tag message id: release_notes env: # e.g. v0.1.0a1, v1.2.0b2 or v2.3.0rc3, but not v1.0.0 PRERELEASE_TAG_PATTERN: "v[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+([ab]|rc)[[:digit:]]+" run: | # GH checkout action doesn't preserve tag annotations, we must fetch them # https://github.com/actions/checkout/issues/290 git fetch --tags --force # strip leading 'refs/tags/' to get the tag name TAG_NAME="${GITHUB_REF##*/}" # Dump tag message to temporary .md file (excluding the PGP signature at the bottom) TAG_MESSAGE=$(git tag -l --format='%(contents)' $TAG_NAME | sed -n '/-----BEGIN PGP SIGNATURE-----/q;p') echo "$TAG_MESSAGE" > "${{ runner.temp }}/release_notes.md" # if the tag has a pre-release suffix mark the Github Release accordingly if egrep -q "$PRERELEASE_TAG_PATTERN" <<< "$TAG_NAME"; then echo "Tag contains a pre-release suffix" echo "IS_PRERELEASE=true" >> "$GITHUB_ENV" else echo "Tag does not contain pre-release suffix" echo "IS_PRERELEASE=false" >> "$GITHUB_ENV" fi - name: Publish distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - name: Create GitHub release id: create_release uses: actions/create-release@v1 env: # This token is provided by Actions, you do not need to create your own token GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} body_path: "${{ runner.temp }}/release_notes.md" draft: false prerelease: ${{ env.IS_PRERELEASE }} robotools-fontPens-7a4c5bc/.gitignore000066400000000000000000000005221520755626700200260ustar00rootroot00000000000000# 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/ # virtual environment venv .venv # vcs Lib/fontPens/_version.py robotools-fontPens-7a4c5bc/.pre-commit-config.yaml000066400000000000000000000007311520755626700223210ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 25.11.0 hooks: - id: black - repo: https://github.com/pycqa/isort rev: 7.0.0 hooks: - id: isort args: ["--profile", "black"] - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml robotools-fontPens-7a4c5bc/LICENSE.txt000066400000000000000000000027341520755626700176700ustar00rootroot00000000000000Copyright 2005-2026 Erik van Blokland, Tal Leming, Just van Rossum Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. 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. 3. Neither the name of the copyright holder 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. robotools-fontPens-7a4c5bc/Lib/000077500000000000000000000000001520755626700165455ustar00rootroot00000000000000robotools-fontPens-7a4c5bc/Lib/fontPens/000077500000000000000000000000001520755626700203415ustar00rootroot00000000000000robotools-fontPens-7a4c5bc/Lib/fontPens/__init__.py000066400000000000000000000001541520755626700224520ustar00rootroot00000000000000try: from ._version import version as __version__ except ImportError: __version__ = "0.0.0+unknown" robotools-fontPens-7a4c5bc/Lib/fontPens/angledMarginPen.py000066400000000000000000000075401520755626700237540ustar00rootroot00000000000000import math 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 _lineTo(self, pt): self._getAngled(pt) self.currentPoint = pt def _curveToOne(self, pt1, pt2, pt3): 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 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) glyph.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) glyph.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) glyph.draw(pen) isLeft, isRight = pen.margin setAngledLeftMargin(glyph, font, (isLeft + isRight) * 0.5) setAngledRightMargin(glyph, font, (isLeft + isRight) * 0.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) * 0.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() robotools-fontPens-7a4c5bc/Lib/fontPens/digestPointPen.py000066400000000000000000000063631520755626700236570ustar00rootroot00000000000000from 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") 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',) >>> pen.addPoint((10, 10), "move", True) >>> pen.addPoint((-10, 100), "line", False) >>> pen.endPath() >>> pen.getDigest() ('beginPath', ((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', ((10, 10), 'move', True, None), ((-10, 100), 'line', False, None), 'endPath', 'beginPath', ((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)) """ # noqa: E501 if __name__ == "__main__": import doctest doctest.testmod() robotools-fontPens-7a4c5bc/Lib/fontPens/flattenPen.py000066400000000000000000000315501520755626700230170ustar00rootroot00000000000000from fontTools.misc.bezierTools import ( calcCubicArcLength, calcQuadraticArcLength, splitCubicAtT, ) from fontTools.pens.basePen import BasePen from fontPens.penTools import ( distance, estimateCubicCurveLength, getCubicPoint, getQuadraticPoint, interpolatePoint, ) 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 class SplittedPen(BasePen): """ This filter pen processes the contours into a series of curves by splitting the curves at an given segment length. - otherPen: a different segment pen object this filter should draw the results with. - approximateSegmentLength: the length you want the flattened segments to be (roughly). """ def __init__(self, otherPen, approximateSegmentLength=5): self.approximateSegmentLength = approximateSegmentLength super().__init__({}) self.otherPen = otherPen def _moveTo(self, pt): self.otherPen.moveTo(pt) self.currentPt = pt self.firstPt = pt def _lineTo(self, pt): 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 maxSteps = int( round( calcCubicArcLength(self.currentPt, pt1, pt2, pt3) / self.approximateSegmentLength ) ) if maxSteps < 1: self.otherPen.lineTo(pt3) self.currentPt = pt3 return tValues = [i / maxSteps for i in range(1, maxSteps)] for curve in splitCubicAtT(self.currentPt, pt1, pt2, pt3, *tValues): self.otherPen.curveTo(*curve[1:]) self.currentPt = pt3 def _closePath(self): self.lineTo(self.firstPt) self.otherPen.closePath() def _endPath(self): self.otherPen.endPath() def addComponent(self, glyphName, transformation): self.otherPen.addComponent(glyphName, transformation) def splittedGlyph(aGlyph, threshold=10): """ Convenience function that applies the **SplittedPen** pen to a glyph in place. """ if len(aGlyph) == 0: return aGlyph from fontTools.pens.recordingPen import RecordingPen recorder = RecordingPen() filterpen = SplittedPen(recorder, approximateSegmentLength=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 = 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 _testFlattenPen2(): """ >>> 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 _testFlattenGlyph2(): """ >>> 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() """ def _makeTestSplittedGlyphWithCurve(): # 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((0, 0)) pen.curveTo((0, 200), (200, 300), (300, 300)) pen.endPath() return testGlyph def _testSplittedGlyph(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestSplittedGlyphWithCurve() >>> splittedGlyph(glyph, threshold=100) #doctest: +ELLIPSIS >> glyph.draw(PrintPen()) pen.moveTo((0, 0)) pen.curveTo((0.0, 40.0), (8.000000000000002, 76.0), (21.6, 108.0)) pen.curveTo((35.2, 140.0), (54.400000000000006, 168.0), (76.80000000000001, 192.0)) pen.curveTo((99.2, 216.0), (124.8, 236.0), (151.2, 252.0)) pen.curveTo((177.6, 268.0), (204.8, 280.0), (230.4, 288.0)) pen.curveTo((256.0, 296.0), (280.0, 300.0), (300, 300)) pen.endPath() """ if __name__ == "__main__": import doctest doctest.testmod() robotools-fontPens-7a4c5bc/Lib/fontPens/guessSmoothPointPen.py000066400000000000000000000074661520755626700247250ustar00rootroot00000000000000import 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() robotools-fontPens-7a4c5bc/Lib/fontPens/marginPen.py000066400000000000000000000135561520755626700226450ustar00rootroot00000000000000from fontTools.misc.bezierTools import splitCubic, splitLine from fontTools.pens.basePen import BasePen 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 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() return testGlyph def _testMarginPen(): """ >>> glyph = _makeTestGlyph() >>> pen = MarginPen(dict(), 200, isHorizontal=True) >>> glyph.draw(pen) >>> pen.getAll() [107.5475, 900.0] """ def _makeTestFont(): # make a simple glyph that we can test the pens with. from fontParts.fontshell import RFont testFont = RFont() baseGlyph = testFont.newGlyph("baseGlyph") pen = baseGlyph.getPen() pen.moveTo((100, 100)) pen.lineTo((600, 100)) pen.lineTo((600, 600)) pen.lineTo((100, 600)) pen.closePath() testGlyph = testFont.newGlyph("testGlyph") testGlyph.appendComponent("baseGlyph", scale=0.5) return testGlyph def _testMarginPenComponent(): """ >>> glyph = _makeTestFont() >>> pen = MarginPen(glyph.layer, 200, isHorizontal=True) >>> glyph.draw(pen) >>> pen.getAll() [50.0, 300.0] """ if __name__ == "__main__": import doctest doctest.testmod() robotools-fontPens-7a4c5bc/Lib/fontPens/penTools.py000066400000000000000000000137571520755626700225330ustar00rootroot00000000000000import math 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)] """ # noqa E501 (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 """ # noqa E501 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() robotools-fontPens-7a4c5bc/Lib/fontPens/printPen.py000066400000000000000000000046771520755626700225300ustar00rootroot00000000000000from 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() robotools-fontPens-7a4c5bc/Lib/fontPens/printPointPen.py000066400000000000000000000044671520755626700235370ustar00rootroot00000000000000from 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() robotools-fontPens-7a4c5bc/Lib/fontPens/recordingPointPen.py000066400000000000000000000040171520755626700243460ustar00rootroot00000000000000from 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() robotools-fontPens-7a4c5bc/Lib/fontPens/spikePen.py000066400000000000000000000116051520755626700224740ustar00rootroot00000000000000from math import atan2, cos, pi, sin from fontTools.pens.basePen import BasePen from fontPens.flattenPen import FlattenPen class SpikePen(BasePen): """ Add narly spikes or dents to the glyph. patternFunc is an optional function which recalculates the offset. """ def __init__(self, otherPen, segmentLength=20, spikeLength=40, patternFunc=None): self.otherPen = otherPen self.segmentLength = segmentLength self.spikeLength = spikeLength self.patternFunc = patternFunc def _moveTo(self, pt): self._points = [pt] def _lineTo(self, pt): self._points.append(pt) def _processPoints(self): pointCount = len(self._points) for i in range(0, pointCount): x, y = self._points[i] if not i % 2: previousPoint = self._points[i - 1] nextPoint = self._points[(i + 1) % pointCount] angle = atan2( previousPoint[0] - nextPoint[0], previousPoint[1] - nextPoint[1] ) if self.patternFunc is not None: thisSpikeLength = self.patternFunc(self.spikeLength) else: thisSpikeLength = self.spikeLength x -= sin(angle + 0.5 * pi) * thisSpikeLength y -= cos(angle + 0.5 * pi) * thisSpikeLength if i == 0: self.otherPen.moveTo((x, y)) else: self.otherPen.lineTo((x, y)) def closePath(self): # pop last point self._points.pop() self._processPoints() self.otherPen.closePath() def endPath(self): self._processPoints() self.otherPen.endPath() def spikeGlyph(aGlyph, segmentLength=20, spikeLength=40, patternFunc=None): from fontTools.pens.recordingPen import RecordingPen recorder = RecordingPen() spikePen = SpikePen(recorder, spikeLength=spikeLength, patternFunc=patternFunc) filterPen = FlattenPen( spikePen, approximateSegmentLength=segmentLength, segmentLines=True ) aGlyph.draw(filterPen) aGlyph.clear() recorder.replay(aGlyph.getPen()) return aGlyph # ========= # = tests = # ========= def _makeTestGlyphRect(): # 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((100, 100)) pen.lineTo((100, 300)) pen.lineTo((300, 300)) pen.lineTo((300, 100)) pen.closePath() return testGlyph def _makeTestGlyphLine(): from fontParts.fontshell import RGlyph testGlyph = RGlyph() testGlyph.name = "testGlyph" testGlyph.width = 500 testPen = testGlyph.getPen() testPen.moveTo((10, 0)) testPen.lineTo((30, 0)) testPen.lineTo((50, 0)) testPen.lineTo((70, 0)) testPen.lineTo((90, 0)) testPen.endPath() return testGlyph def _testSpikePen(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyphLine() >>> pen = SpikePen(PrintPen()) >>> glyph.draw(pen) pen.moveTo((9.999999999999995, 40.0)) pen.lineTo((30, 0)) pen.lineTo((50.0, -40.0)) pen.lineTo((70, 0)) pen.lineTo((90.0, 40.0)) pen.endPath() """ def _testSpikeGlyph(): """ >>> from fontPens.printPen import PrintPen >>> glyph = _makeTestGlyphRect() >>> spikeGlyph(glyph) #doctest: +ELLIPSIS >> glyph.draw(PrintPen()) pen.moveTo((128.2842712474619, 128.2842712474619)) pen.lineTo((100.0, 120.0)) pen.lineTo((140.0, 140.0)) pen.lineTo((100.0, 160.0)) pen.lineTo((140.0, 180.0)) pen.lineTo((100.0, 200.0)) pen.lineTo((140.0, 220.0)) pen.lineTo((100.0, 240.0)) pen.lineTo((140.0, 260.0)) pen.lineTo((100.0, 280.0)) pen.lineTo((128.2842712474619, 271.7157287525381)) pen.lineTo((120.0, 300.0)) pen.lineTo((140.0, 260.0)) pen.lineTo((160.0, 300.0)) pen.lineTo((180.0, 260.0)) pen.lineTo((200.0, 300.0)) pen.lineTo((220.0, 260.0)) pen.lineTo((240.0, 300.0)) pen.lineTo((260.0, 260.0)) pen.lineTo((280.0, 300.0)) pen.lineTo((271.7157287525381, 271.7157287525381)) pen.lineTo((300.0, 280.0)) pen.lineTo((260.0, 260.0)) pen.lineTo((300.0, 240.0)) pen.lineTo((260.0, 220.0)) pen.lineTo((300.0, 200.0)) pen.lineTo((260.0, 180.0)) pen.lineTo((300.0, 160.0)) pen.lineTo((260.0, 140.0)) pen.lineTo((300.0, 120.0)) pen.lineTo((271.7157287525381, 128.2842712474619)) pen.lineTo((280.0, 100.0)) pen.lineTo((260.0, 140.0)) pen.lineTo((240.0, 100.0)) pen.lineTo((220.0, 140.0)) pen.lineTo((200.0, 100.0)) pen.lineTo((180.0, 140.0)) pen.lineTo((160.0, 100.0)) pen.lineTo((140.0, 140.0)) pen.lineTo((120.0, 100.0)) pen.closePath() """ if __name__ == "__main__": import doctest doctest.testmod() robotools-fontPens-7a4c5bc/Lib/fontPens/thresholdPen.py000066400000000000000000000057761520755626700233710ustar00rootroot00000000000000from 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() robotools-fontPens-7a4c5bc/Lib/fontPens/thresholdPointPen.py000066400000000000000000000102051520755626700243620ustar00rootroot00000000000000from 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() robotools-fontPens-7a4c5bc/Lib/fontPens/transformPointPen.py000066400000000000000000000031661520755626700244110ustar00rootroot00000000000000from 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() robotools-fontPens-7a4c5bc/README.md000066400000000000000000000013431520755626700173170ustar00rootroot00000000000000# fontPens A collection of classes implementing the pen protocol for manipulating glyph outlines. ## Maintainers: how to release To cut a release, make an annotated git tag, where the tag is in this format: v1.2.3, where 1, 2 and 3 represent major, minor and micro version numbers. You can add "aN" or "bN" or "rc" to mark alpha, beta or "release candidate" versions. Examples: v1.2.3, v1.2.3b2, v1.2.3a4, v1.2.3rc. The message for the annotated tag should contain the release notes. Then use "git push --follow-tags" to trigger the release bot. Example session: - `$ git tag -a v1.2.3 -m "v1.2.3 -- fixed issue #12345"` - `$ git push --follow-tags` This process will create a GitHub release, as well as upload the package to PyPI. robotools-fontPens-7a4c5bc/pyproject.toml000066400000000000000000000026271520755626700207620ustar00rootroot00000000000000[build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "fontPens" description = "A collection of classes implementing the pen protocol for manipulating glyphs." readme = "README.md" authors = [ { name = "Tal Leming" }, { name = "Erik van Blokland" }, { name = "Ben Kiel" }, { name = "Frederik Berlaen" }, { name = "Just van Rossum", email = "justvanrossum@gmail.com" } ] keywords = ["font", "fonts"] license = "BSD-3-Clause" license-files = ["LICENSE.txt"] dynamic = ["version"] requires-python = ">=3.10" dependencies = [ "fonttools>=4.40", ] classifiers = [ "Development Status :: 5 - Production/Stable", "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", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics :: Graphics Conversion", ] [tool.hatch.build.targets.wheel] packages = ["Lib/fontPens"] [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] version-file = "Lib/fontPens/_version.py" [tool.pytest] minversion = "9.0" addopts = ["-ra", "-v", "--doctest-modules"] testpaths = ["Lib/fontPens"] robotools-fontPens-7a4c5bc/requirements-dev.txt000066400000000000000000000000401520755626700220710ustar00rootroot00000000000000pytest==9.0.3 fontParts>=0.14.2 robotools-fontPens-7a4c5bc/requirements.txt000066400000000000000000000000221520755626700213150ustar00rootroot00000000000000fonttools==4.63.0