├── .coveragerc ├── .github └── workflows │ ├── publish-package.yml │ └── run-tests.yml ├── .gitignore ├── .pyup.yml ├── Lib └── fontMath │ ├── __init__.py │ ├── mathFunctions.py │ ├── mathGlyph.py │ ├── mathGuideline.py │ ├── mathInfo.py │ ├── mathKerning.py │ ├── mathTransform.py │ └── test │ ├── __init__.py │ ├── test_mathFunctions.py │ ├── test_mathGlyph.py │ ├── test_mathGuideline.py │ ├── test_mathInfo.py │ ├── test_mathKerning.py │ └── test_mathTransform.py ├── License.txt ├── MANIFEST.in ├── README.md ├── dev-requirements.txt ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | # measure 'branch' coverage in addition to 'statement' coverage 3 | # See: http://coverage.readthedocs.org/en/coverage-4.0.3/branch.html#branch 4 | branch = True 5 | 6 | # list of directories to measure 7 | source = 8 | Lib/fontMath 9 | 10 | [report] 11 | # Regexes for lines to exclude from consideration 12 | exclude_lines = 13 | # keywords to use in inline comments to skip coverage 14 | pragma: no cover 15 | 16 | # don't complain if tests don't hit defensive assertion code 17 | raise AssertionError 18 | raise NotImplementedError 19 | 20 | # don't complain if non-runnable code isn't run 21 | if 0: 22 | if __name__ == .__main__.: 23 | 24 | # ignore source code that can’t be found 25 | ignore_errors = True 26 | 27 | # when running a summary report, show missing lines 28 | show_missing = True 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-package.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Python Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9].*' 7 | 8 | jobs: 9 | create_release: 10 | name: Create GitHub Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Create release 14 | uses: actions/create-release@v1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | with: 18 | tag_name: ${{ github.ref }} 19 | release_name: ${{ github.ref }} 20 | draft: false 21 | prerelease: true 22 | 23 | deploy: 24 | 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v3.5.3 29 | - name: Set up Python 30 | uses: actions/setup-python@v4.7.0 31 | with: 32 | python-version: '3.x' 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install setuptools wheel twine 37 | - name: Build and publish 38 | env: 39 | TWINE_USERNAME: __token__ 40 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 41 | run: | 42 | python setup.py sdist bdist_wheel 43 | twine upload dist/* 44 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | inputs: 10 | reason: 11 | description: 'Reason for running workflow' 12 | required: true 13 | 14 | jobs: 15 | 16 | test: 17 | runs-on: ${{ matrix.platform }} 18 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 19 | strategy: 20 | matrix: 21 | python-version: ['3.8', '3.9', '3.10', '3.11'] 22 | platform: [ubuntu-latest, macos-latest, windows-latest] 23 | exclude: # Only test on the oldest and latest supported stable Python on macOS and Windows. 24 | - platform: macos-latest 25 | python-version: 3.9 26 | - platform: windows-latest 27 | python-version: 3.9 28 | - platform: macos-latest 29 | python-version: 3.10 30 | - platform: windows-latest 31 | python-version: 3.10 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v2 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | - name: Install packages 39 | run: pip install tox coverage 40 | - name: Run Tox 41 | run: tox -e py-cov 42 | - name: Produce coverage files 43 | run: | 44 | coverage combine 45 | coverage xml 46 | - name: Upload coverage to Codecov 47 | uses: codecov/codecov-action@v4 48 | with: 49 | file: coverage.xml 50 | flags: unittests 51 | name: codecov-umbrella 52 | fail_ci_if_error: true 53 | token: ${{ secrets.CODECOV_TOKEN }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled and optimized files 2 | __pycache__/ 3 | *.py[co] 4 | *$py.class 5 | 6 | # Distribution / Packaging 7 | *.egg-info 8 | *.eggs 9 | MANIFEST 10 | build/ 11 | dist/ 12 | 13 | # Unit test and coverage files 14 | .cache/ 15 | .coverage 16 | .coverage.* 17 | .tox/ 18 | htmlcov/ 19 | 20 | # OS X Finder 21 | .DS_Store 22 | .pytest_cache 23 | 24 | # auto-generated version file 25 | Lib/fontMath/_version.py -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # controls the frequency of updates (undocumented beta feature) 2 | schedule: every week 3 | 4 | # do not pin dependencies unless they have explicit version specifiers 5 | pin: False 6 | -------------------------------------------------------------------------------- /Lib/fontMath/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A set of fast glyph and font objects for math operations. 3 | 4 | This was inspired, and is more or less a clone, of Erik van Blokland's 5 | brilliant glyph math in RoboFab. 6 | """ 7 | 8 | try: 9 | from ._version import __version__ 10 | except ImportError: 11 | try: 12 | from setuptools_scm import get_version 13 | __version__ = get_version() 14 | except ImportError: 15 | __version__ = 'unknown' 16 | 17 | from fontMath.mathGlyph import MathGlyph 18 | from fontMath.mathInfo import MathInfo 19 | from fontMath.mathKerning import MathKerning 20 | -------------------------------------------------------------------------------- /Lib/fontMath/mathFunctions.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import math 3 | import sys 4 | 5 | __all__ = [ 6 | "add", 7 | "addPt", 8 | "sub", 9 | "subPt", 10 | "mul", 11 | "mulPt", 12 | "div", 13 | "divPt", 14 | "factorAngle", 15 | "_roundNumber", 16 | ] 17 | 18 | def add(v1, v2): 19 | return v1 + v2 20 | 21 | def addPt(pt1, pt2): 22 | return pt1[0] + pt2[0], pt1[1] + pt2[1] 23 | 24 | def sub(v1, v2): 25 | return v1 - v2 26 | 27 | def subPt(pt1, pt2): 28 | return pt1[0] - pt2[0], pt1[1] - pt2[1] 29 | 30 | def mul(v, f): 31 | return v * f 32 | 33 | def mulPt(pt1, f): 34 | (f1, f2) = f 35 | return pt1[0] * f1, pt1[1] * f2 36 | 37 | def div(v, f): 38 | return v / f 39 | 40 | def divPt(pt, f): 41 | (f1, f2) = f 42 | return pt[0] / f1, pt[1] / f2 43 | 44 | def factorAngle(angle, f, func): 45 | (f1, f2) = f 46 | # If both factors are equal, assume a scalar factor and scale the angle as such. 47 | if f1 == f2: 48 | return func(angle, f1) 49 | # Otherwise, scale x and y components separately. 50 | rangle = math.radians(angle) 51 | x = math.cos(rangle) 52 | y = math.sin(rangle) 53 | return math.degrees( 54 | math.atan2( 55 | func(y, f2), func(x, f1) 56 | ) 57 | ) 58 | 59 | 60 | def round2(number, ndigits=None): 61 | """ 62 | Implementation of Python 2 built-in round() function. 63 | Rounds a number to a given precision in decimal digits (default 64 | 0 digits). The result is a floating point number. Values are rounded 65 | to the closest multiple of 10 to the power minus ndigits; if two 66 | multiples are equally close, rounding is done away from 0. 67 | ndigits may be negative. 68 | See Python 2 documentation: 69 | https://docs.python.org/2/library/functions.html?highlight=round#round 70 | 71 | This is taken from the to-be-deprecated py23 fuction of fonttools 72 | """ 73 | import decimal as _decimal 74 | 75 | if ndigits is None: 76 | ndigits = 0 77 | 78 | if ndigits < 0: 79 | exponent = 10 ** (-ndigits) 80 | quotient, remainder = divmod(number, exponent) 81 | if remainder >= exponent // 2 and number >= 0: 82 | quotient += 1 83 | return float(quotient * exponent) 84 | else: 85 | exponent = _decimal.Decimal("10") ** (-ndigits) 86 | 87 | d = _decimal.Decimal.from_float(number).quantize( 88 | exponent, rounding=_decimal.ROUND_HALF_UP 89 | ) 90 | 91 | return float(d) 92 | 93 | 94 | def setRoundIntegerFunction(func): 95 | """ Globally set function for rounding floats to integers. 96 | 97 | The function signature must be: 98 | 99 | def func(value: float) -> int 100 | """ 101 | global _ROUND_INTEGER_FUNC 102 | _ROUND_INTEGER_FUNC = func 103 | 104 | 105 | def setRoundFloatFunction(func): 106 | """ Globally set function for rounding floats within given precision. 107 | 108 | The function signature must be: 109 | 110 | def func(value: float, ndigits: int) -> float 111 | """ 112 | global _ROUND_FLOAT_FUNC 113 | _ROUND_FLOAT_FUNC = func 114 | 115 | 116 | _ROUND_INTEGER_FUNC = round 117 | _ROUND_FLOAT_FUNC = round 118 | 119 | 120 | def _roundNumber(value, ndigits=None): 121 | """Round number using the Python 3 built-in round function. 122 | 123 | You can change the default rounding functions using setRoundIntegerFunction 124 | and/or setRoundFloatFunction. 125 | """ 126 | if ndigits is not None: 127 | return _ROUND_FLOAT_FUNC(value, ndigits) 128 | return _ROUND_INTEGER_FUNC(value) 129 | 130 | 131 | if __name__ == "__main__": 132 | import sys 133 | import doctest 134 | sys.exit(doctest.testmod().failed) 135 | -------------------------------------------------------------------------------- /Lib/fontMath/mathGlyph.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import 2 | from copy import deepcopy 3 | from collections import OrderedDict 4 | from fontMath.mathFunctions import ( 5 | add, addPt, div, divPt, mul, mulPt, _roundNumber, sub, subPt) 6 | from fontMath.mathGuideline import ( 7 | _compressGuideline, _expandGuideline, _pairGuidelines, 8 | _processMathOneGuidelines, _processMathTwoGuidelines, _roundGuidelines) 9 | from fontTools.pens.pointPen import AbstractPointPen 10 | 11 | # ------------------ 12 | # UFO 3 branch notes 13 | # ------------------ 14 | # 15 | # to do: 16 | # X components 17 | # X identifiers 18 | # X contours 19 | # X identifiers 20 | # X points 21 | # X identifiers 22 | # X guidelines 23 | # X height 24 | # X image 25 | # 26 | # - is there any cruft that can be removed? 27 | # X why is divPt here? move all of those to the math functions 28 | # - FilterRedundantPointPen._flushContour is a mess 29 | # X for the pt math functions, always send (x, y) factors instead 30 | # of coercing within the function. the coercion can happen at 31 | # the beginning of the _processMathTwo method. 32 | # - try list comprehensions in the point math for speed 33 | # 34 | # Questionable stuff: 35 | # X is getRef needed? 36 | # X nothing is ever set to _structure. should it be? 37 | # X should the compatibilty be a function or pen? 38 | # X the lib import is shallow and modifications to 39 | # lower level objects (ie dict) could modify the 40 | # original object. this probably isn't desirable. 41 | # deepcopy won't work here since it will try to 42 | # maintain the original class. may need to write 43 | # a custom copier. or maybe something like this 44 | # would be sufficient: 45 | # self.lib = deepcopy(dict(glyph.lib)) 46 | # the class would be maintained for everything but 47 | # the top level. that shouldn't matter for the 48 | # purposes here. 49 | # - __cmp__ is dubious but harmless i suppose. 50 | # X is generationCount needed? 51 | # X can box become bounds? have both? 52 | 53 | try: 54 | basestring, xrange 55 | range = xrange 56 | except NameError: 57 | basestring = str 58 | 59 | 60 | class MathGlyph(object): 61 | 62 | """ 63 | A very shallow glyph object for rapid math operations. 64 | 65 | Notes about glyph math: 66 | - absolute contour compatibility is required 67 | - absolute component, anchor, guideline and image compatibility is NOT required. 68 | in cases of incompatibility in this data, only compatible data is processed and 69 | returned. because of this, anchors and components may not be returned in the 70 | same order as the original. 71 | """ 72 | 73 | def __init__(self, glyph, scaleComponentTransform=True, strict=False): 74 | """Initialize a new MathGlyph object. 75 | 76 | Args: 77 | glyph: Input defcon or defcon-like Glyph object to copy from. Set to None to 78 | to make an empty MathGlyph. 79 | scaleComponentTransform (bool): when performing multiplication or division, by 80 | default all elements of a component's affine transformation matrix are 81 | multiplied by the given scalar. If scaleComponentTransform is False, then 82 | only the component's xOffset and yOffset attributes are scaled, whereas the 83 | xScale, xyScale, yxScale and yScale attributes are kept unchanged. 84 | strict (bool): when set to False, offcurve points will be added to all 85 | straight segments to improve compatibility. Any offcurves that are 86 | still on-point will be filtered when extracted. When set to True, 87 | no offcurves will be added or filtered. 88 | """ 89 | self.scaleComponentTransform = scaleComponentTransform 90 | self.contours = [] 91 | self.components = [] 92 | self.strict = strict 93 | if glyph is None: 94 | self.anchors = [] 95 | self.guidelines = [] 96 | self.image = _expandImage(None) 97 | self.lib = {} 98 | self.name = None 99 | self.unicodes = None 100 | self.width = None 101 | self.height = None 102 | self.note = None 103 | else: 104 | p = MathGlyphPen(self, strict=self.strict) 105 | glyph.drawPoints(p) 106 | self.anchors = [dict(anchor) for anchor in glyph.anchors] 107 | self.guidelines = [_expandGuideline(guideline) for guideline in glyph.guidelines] 108 | self.image = _expandImage(glyph.image) 109 | self.lib = deepcopy(dict(glyph.lib)) 110 | self.name = glyph.name 111 | self.unicodes = list(glyph.unicodes) 112 | self.width = glyph.width 113 | self.height = glyph.height 114 | self.note = glyph.note 115 | 116 | def __eq__(self, other): 117 | try: 118 | return all(getattr(self, attr) == getattr(other, attr) 119 | for attr in ("name", "unicodes", "width", "height", 120 | "note", "lib", "contours", "components", 121 | "anchors", "guidelines", "image")) 122 | except AttributeError: 123 | return NotImplemented 124 | 125 | def __ne__(self, other): 126 | return not self == other 127 | 128 | # ---- 129 | # Copy 130 | # ---- 131 | 132 | def copy(self): 133 | """return a new MathGlyph containing all data in self""" 134 | return MathGlyph(self, scaleComponentTransform=self.scaleComponentTransform, strict=self.strict) 135 | 136 | def copyWithoutMathSubObjects(self): 137 | """ 138 | return a new MathGlyph containing all data except: 139 | contours 140 | components 141 | anchors 142 | guidelines 143 | 144 | this is used mainly for internal glyph math. 145 | """ 146 | n = MathGlyph(None, scaleComponentTransform=self.scaleComponentTransform, strict=self.strict) 147 | n.name = self.name 148 | if self.unicodes is not None: 149 | n.unicodes = list(self.unicodes) 150 | n.width = self.width 151 | n.height = self.height 152 | n.note = self.note 153 | n.lib = deepcopy(dict(self.lib)) 154 | return n 155 | 156 | # ---- 157 | # Math 158 | # ---- 159 | 160 | # math with other glyph 161 | 162 | def __add__(self, otherGlyph): 163 | copiedGlyph = self.copyWithoutMathSubObjects() 164 | self._processMathOne(copiedGlyph, otherGlyph, addPt, add) 165 | return copiedGlyph 166 | 167 | def __sub__(self, otherGlyph): 168 | copiedGlyph = self.copyWithoutMathSubObjects() 169 | self._processMathOne(copiedGlyph, otherGlyph, subPt, sub) 170 | return copiedGlyph 171 | 172 | def _processMathOne(self, copiedGlyph, otherGlyph, ptFunc, func): 173 | # width 174 | copiedGlyph.width = func(self.width, otherGlyph.width) 175 | # height 176 | copiedGlyph.height = func(self.height, otherGlyph.height) 177 | # contours 178 | copiedGlyph.contours = [] 179 | if self.contours: 180 | copiedGlyph.contours = _processMathOneContours(self.contours, otherGlyph.contours, ptFunc) 181 | # components 182 | copiedGlyph.components = [] 183 | if self.components: 184 | componentPairs = _pairComponents(self.components, otherGlyph.components) 185 | copiedGlyph.components = _processMathOneComponents(componentPairs, ptFunc) 186 | # anchors 187 | copiedGlyph.anchors = [] 188 | if self.anchors: 189 | anchorTree1 = _anchorTree(self.anchors) 190 | anchorTree2 = _anchorTree(otherGlyph.anchors) 191 | anchorPairs = _pairAnchors(anchorTree1, anchorTree2) 192 | copiedGlyph.anchors = _processMathOneAnchors(anchorPairs, ptFunc) 193 | # guidelines 194 | copiedGlyph.guidelines = [] 195 | if self.guidelines: 196 | guidelinePairs = _pairGuidelines(self.guidelines, otherGlyph.guidelines) 197 | copiedGlyph.guidelines = _processMathOneGuidelines(guidelinePairs, ptFunc, func) 198 | # image 199 | copiedGlyph.image = _expandImage(None) 200 | imagePair = _pairImages(self.image, otherGlyph.image) 201 | if imagePair: 202 | copiedGlyph.image = _processMathOneImage(imagePair, ptFunc) 203 | 204 | # math with factor 205 | 206 | def __mul__(self, factor): 207 | if not isinstance(factor, tuple): 208 | factor = (factor, factor) 209 | copiedGlyph = self.copyWithoutMathSubObjects() 210 | self._processMathTwo(copiedGlyph, factor, mulPt, mul) 211 | return copiedGlyph 212 | 213 | __rmul__ = __mul__ 214 | 215 | def __div__(self, factor): 216 | if not isinstance(factor, tuple): 217 | factor = (factor, factor) 218 | copiedGlyph = self.copyWithoutMathSubObjects() 219 | self._processMathTwo(copiedGlyph, factor, divPt, div) 220 | return copiedGlyph 221 | 222 | __truediv__ = __div__ 223 | 224 | __rdiv__ = __div__ 225 | 226 | __rtruediv__ = __rdiv__ 227 | 228 | def _processMathTwo(self, copiedGlyph, factor, ptFunc, func): 229 | # width 230 | copiedGlyph.width = func(self.width, factor[0]) 231 | # height 232 | copiedGlyph.height = func(self.height, factor[1]) 233 | # contours 234 | copiedGlyph.contours = [] 235 | if self.contours: 236 | copiedGlyph.contours = _processMathTwoContours(self.contours, factor, ptFunc) 237 | # components 238 | copiedGlyph.components = [] 239 | if self.components: 240 | copiedGlyph.components = _processMathTwoComponents( 241 | self.components, factor, ptFunc, scaleComponentTransform=self.scaleComponentTransform 242 | ) 243 | # anchors 244 | copiedGlyph.anchors = [] 245 | if self.anchors: 246 | copiedGlyph.anchors = _processMathTwoAnchors(self.anchors, factor, ptFunc) 247 | # guidelines 248 | copiedGlyph.guidelines = [] 249 | if self.guidelines: 250 | copiedGlyph.guidelines = _processMathTwoGuidelines(self.guidelines, factor, func) 251 | # image 252 | if self.image: 253 | copiedGlyph.image = _processMathTwoImage(self.image, factor, ptFunc) 254 | 255 | # ------- 256 | # Additional math 257 | # ------- 258 | def round(self, digits=None): 259 | """round the geometry.""" 260 | copiedGlyph = self.copyWithoutMathSubObjects() 261 | # misc 262 | copiedGlyph.width = _roundNumber(self.width, digits) 263 | copiedGlyph.height = _roundNumber(self.height, digits) 264 | # contours 265 | copiedGlyph.contours = [] 266 | if self.contours: 267 | copiedGlyph.contours = _roundContours(self.contours, digits) 268 | # components 269 | copiedGlyph.components = [] 270 | if self.components: 271 | copiedGlyph.components = _roundComponents(self.components, digits) 272 | # guidelines 273 | copiedGlyph.guidelines = [] 274 | if self.guidelines: 275 | copiedGlyph.guidelines = _roundGuidelines(self.guidelines, digits) 276 | # anchors 277 | copiedGlyph.anchors = [] 278 | if self.anchors: 279 | copiedGlyph.anchors = _roundAnchors(self.anchors, digits) 280 | # image 281 | copiedGlyph.image = None 282 | if self.image: 283 | copiedGlyph.image = _roundImage(self.image, digits) 284 | return copiedGlyph 285 | 286 | 287 | # ------- 288 | # Pen API 289 | # ------- 290 | 291 | def getPointPen(self): 292 | """get a point pen for drawing to this object""" 293 | return MathGlyphPen(self) 294 | 295 | def drawPoints(self, pointPen, filterRedundantPoints=False): 296 | """draw self using pointPen""" 297 | if filterRedundantPoints: 298 | pointPen = FilterRedundantPointPen(pointPen) 299 | for contour in self.contours: 300 | pointPen.beginPath(identifier=contour["identifier"]) 301 | for segmentType, pt, smooth, name, identifier in contour["points"]: 302 | pointPen.addPoint(pt=pt, segmentType=segmentType, smooth=smooth, name=name, identifier=identifier) 303 | pointPen.endPath() 304 | for component in self.components: 305 | pointPen.addComponent(component["baseGlyph"], component["transformation"], identifier=component["identifier"]) 306 | 307 | def draw(self, pen, filterRedundantPoints=False): 308 | """draw self using pen""" 309 | from fontTools.pens.pointPen import PointToSegmentPen 310 | pointPen = PointToSegmentPen(pen) 311 | self.drawPoints(pointPen, filterRedundantPoints=filterRedundantPoints) 312 | 313 | # ---------- 314 | # Extraction 315 | # ---------- 316 | 317 | def extractGlyph(self, glyph, pointPen=None, onlyGeometry=False): 318 | """ 319 | "rehydrate" to a glyph. this requires 320 | a glyph as an argument. if a point pen other 321 | than the type of pen returned by glyph.getPointPen() 322 | is required for drawing, send this the needed point pen. 323 | """ 324 | if pointPen is None: 325 | pointPen = glyph.getPointPen() 326 | glyph.clearContours() 327 | glyph.clearComponents() 328 | glyph.clearAnchors() 329 | glyph.clearGuidelines() 330 | glyph.lib.clear() 331 | if self.strict: 332 | self.drawPoints(pointPen) 333 | else: 334 | cleanerPen = FilterRedundantPointPen(pointPen) 335 | self.drawPoints(cleanerPen) 336 | glyph.anchors = [dict(anchor) for anchor in self.anchors] 337 | glyph.guidelines = [_compressGuideline(guideline) for guideline in self.guidelines] 338 | glyph.image = _compressImage(self.image) 339 | glyph.lib = deepcopy(dict(self.lib)) 340 | glyph.width = self.width 341 | glyph.height = self.height 342 | glyph.note = self.note 343 | if not onlyGeometry: 344 | glyph.name = self.name 345 | glyph.unicodes = list(self.unicodes) 346 | return glyph 347 | 348 | 349 | # ---------- 350 | # Point Pens 351 | # ---------- 352 | 353 | class MathGlyphPen(AbstractPointPen): 354 | 355 | """ 356 | Point pen for building MathGlyph data structures. 357 | """ 358 | 359 | def __init__(self, glyph=None, strict=False): 360 | self.strict = strict # do not add offcurvess 361 | if glyph is None: 362 | self.contours = [] 363 | self.components = [] 364 | else: 365 | self.contours = glyph.contours 366 | self.components = glyph.components 367 | self._contourIdentifier = None 368 | self._points = [] 369 | 370 | def _flushContour(self): 371 | """ 372 | This normalizes the contour so that: 373 | - there are no line segments. in their place will be 374 | curve segments with the off curves positioned on top 375 | of the previous on curve and the new curve on curve. 376 | - the contour starts with an on curve 377 | """ 378 | self.contours.append( 379 | dict(identifier=self._contourIdentifier, points=[]) 380 | ) 381 | contourPoints = self.contours[-1]["points"] 382 | points = self._points 383 | # move offcurves at the beginning of the contour to the end 384 | if not self.strict: 385 | haveOnCurve = False 386 | for point in points: 387 | if point[0] is not None: 388 | haveOnCurve = True 389 | break 390 | if haveOnCurve: 391 | while 1: 392 | if points[0][0] is None: 393 | point = points.pop(0) 394 | points.append(point) 395 | else: 396 | break 397 | # convert lines to curves 398 | holdingOffCurves = [] 399 | for index, point in enumerate(points): 400 | segmentType = point[0] 401 | if segmentType == "line" and not self.strict: 402 | pt, smooth, name, identifier = point[1:] 403 | prevPt = points[index - 1][1] 404 | if index == 0: 405 | holdingOffCurves.append((None, prevPt, False, None, None)) 406 | holdingOffCurves.append((None, pt, False, None, None)) 407 | else: 408 | contourPoints.append((None, prevPt, False, None, None)) 409 | contourPoints.append((None, pt, False, None, None)) 410 | contourPoints.append(("curve", pt, smooth, name, identifier)) 411 | else: 412 | contourPoints.append(point) 413 | contourPoints.extend(holdingOffCurves) 414 | 415 | def beginPath(self, identifier=None): 416 | self._contourIdentifier = identifier 417 | self._points = [] 418 | 419 | def addPoint(self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): 420 | self._points.append((segmentType, pt, smooth, name, identifier)) 421 | 422 | def endPath(self): 423 | self._flushContour() 424 | 425 | def addComponent(self, baseGlyph, transformation, identifier=None, **kwargs): 426 | self.components.append(dict(baseGlyph=baseGlyph, transformation=transformation, identifier=identifier)) 427 | 428 | 429 | class FilterRedundantPointPen(AbstractPointPen): 430 | 431 | def __init__(self, anotherPointPen): 432 | self._pen = anotherPointPen 433 | self._points = [] 434 | 435 | def _flushContour(self): 436 | # keep the point order and 437 | # change the removed flag if the point should be removed 438 | points = self._points 439 | for index, data in enumerate(points): 440 | if data["segmentType"] == "curve": 441 | prevOnCurve = points[index - 3] 442 | prevOffCurve1 = points[index - 2] 443 | prevOffCurve2 = points[index - 1] 444 | # check if the curve is a super bezier 445 | if prevOnCurve["segmentType"] is not None: 446 | if prevOnCurve["pt"] == prevOffCurve1["pt"] and prevOffCurve2["pt"] == data["pt"]: 447 | # the off curves are on top of the on curve point 448 | # change the segmentType 449 | data["segmentType"] = "line" 450 | # flag the off curves to be removed 451 | prevOffCurve1["removed"] = True 452 | prevOffCurve2["removed"] = True 453 | 454 | for data in points: 455 | if not data["removed"]: 456 | self._pen.addPoint( 457 | data["pt"], 458 | data["segmentType"], 459 | smooth=data["smooth"], 460 | name=data["name"], 461 | identifier=data["identifier"] 462 | ) 463 | 464 | def beginPath(self, identifier=None, **kwargs): 465 | self._points = [] 466 | self._pen.beginPath(identifier=identifier) 467 | 468 | def addPoint(self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): 469 | self._points.append( 470 | dict( 471 | pt=pt, 472 | segmentType=segmentType, 473 | smooth=smooth, 474 | name=name, 475 | identifier=identifier, 476 | removed=False 477 | ) 478 | ) 479 | 480 | def endPath(self): 481 | self._flushContour() 482 | self._pen.endPath() 483 | 484 | def addComponent(self, baseGlyph, transformation, identifier=None, **kwargs): 485 | self._pen.addComponent(baseGlyph, transformation, identifier) 486 | 487 | 488 | # ------- 489 | # Support 490 | # ------- 491 | 492 | # contours 493 | 494 | def _processMathOneContours(contours1, contours2, func): 495 | result = [] 496 | for index, contour1 in enumerate(contours1): 497 | contourIdentifier = contour1["identifier"] 498 | points1 = contour1["points"] 499 | points2 = contours2[index]["points"] 500 | resultPoints = [] 501 | for index, point in enumerate(points1): 502 | segmentType, pt1, smooth, name, identifier = point 503 | pt2 = points2[index][1] 504 | pt = func(pt1, pt2) 505 | resultPoints.append((segmentType, pt, smooth, name, identifier)) 506 | result.append(dict(identifier=contourIdentifier, points=resultPoints)) 507 | return result 508 | 509 | def _processMathTwoContours(contours, factor, func): 510 | result = [] 511 | for contour in contours: 512 | contourIdentifier = contour["identifier"] 513 | points = contour["points"] 514 | resultPoints = [] 515 | for point in points: 516 | segmentType, pt, smooth, name, identifier = point 517 | pt = func(pt, factor) 518 | resultPoints.append((segmentType, pt, smooth, name, identifier)) 519 | result.append(dict(identifier=contourIdentifier, points=resultPoints)) 520 | return result 521 | 522 | # anchors 523 | 524 | def _anchorTree(anchors): 525 | tree = OrderedDict() 526 | for anchor in anchors: 527 | x = anchor["x"] 528 | y = anchor["y"] 529 | name = anchor.get("name") 530 | identifier = anchor.get("identifier") 531 | color = anchor.get("color") 532 | if name not in tree: 533 | tree[name] = [] 534 | tree[name].append((identifier, x, y, color)) 535 | return tree 536 | 537 | def _pairAnchors(anchorDict1, anchorDict2): 538 | """ 539 | Anchors are paired using the following rules: 540 | 541 | 542 | Matching Identifiers 543 | -------------------- 544 | >>> anchors1 = { 545 | ... "test" : [ 546 | ... (None, 1, 2, None), 547 | ... ("identifier 1", 3, 4, None) 548 | ... ] 549 | ... } 550 | >>> anchors2 = { 551 | ... "test" : [ 552 | ... ("identifier 1", 1, 2, None), 553 | ... (None, 3, 4, None) 554 | ... ] 555 | ... } 556 | >>> expected = [ 557 | ... ( 558 | ... dict(name="test", identifier=None, x=1, y=2, color=None), 559 | ... dict(name="test", identifier=None, x=3, y=4, color=None) 560 | ... ), 561 | ... ( 562 | ... dict(name="test", identifier="identifier 1", x=3, y=4, color=None), 563 | ... dict(name="test", identifier="identifier 1", x=1, y=2, color=None) 564 | ... ) 565 | ... ] 566 | >>> _pairAnchors(anchors1, anchors2) == expected 567 | True 568 | 569 | Mismatched Identifiers 570 | ---------------------- 571 | >>> anchors1 = { 572 | ... "test" : [ 573 | ... ("identifier 1", 3, 4, None) 574 | ... ] 575 | ... } 576 | >>> anchors2 = { 577 | ... "test" : [ 578 | ... ("identifier 2", 1, 2, None), 579 | ... ] 580 | ... } 581 | >>> expected = [ 582 | ... ( 583 | ... dict(name="test", identifier="identifier 1", x=3, y=4, color=None), 584 | ... dict(name="test", identifier="identifier 2", x=1, y=2, color=None) 585 | ... ) 586 | ... ] 587 | >>> _pairAnchors(anchors1, anchors2) == expected 588 | True 589 | """ 590 | pairs = [] 591 | for name, anchors1 in anchorDict1.items(): 592 | if name not in anchorDict2: 593 | continue 594 | anchors2 = anchorDict2[name] 595 | # align with matching identifiers 596 | removeFromAnchors1 = [] 597 | for anchor1 in anchors1: 598 | match = None 599 | identifier = anchor1[0] 600 | for anchor2 in anchors2: 601 | if anchor2[0] == identifier: 602 | match = anchor2 603 | break 604 | if match is not None: 605 | anchor2 = match 606 | anchors2.remove(anchor2) 607 | removeFromAnchors1.append(anchor1) 608 | a1 = dict(name=name, identifier=identifier) 609 | a1["x"], a1["y"], a1["color"] = anchor1[1:] 610 | a2 = dict(name=name, identifier=identifier) 611 | a2["x"], a2["y"], a2["color"] = anchor2[1:] 612 | pairs.append((a1, a2)) 613 | for anchor1 in removeFromAnchors1: 614 | anchors1.remove(anchor1) 615 | if not anchors1 or not anchors2: 616 | continue 617 | # align by index 618 | while 1: 619 | anchor1 = anchors1.pop(0) 620 | anchor2 = anchors2.pop(0) 621 | a1 = dict(name=name) 622 | a1["identifier"], a1["x"], a1["y"], a1["color"] = anchor1 623 | a2 = dict(name=name, identifier=identifier) 624 | a2["identifier"], a2["x"], a2["y"], a2["color"] = anchor2 625 | pairs.append((a1, a2)) 626 | if not anchors1: 627 | break 628 | if not anchors2: 629 | break 630 | return pairs 631 | 632 | def _processMathOneAnchors(anchorPairs, func): 633 | result = [] 634 | for anchor1, anchor2 in anchorPairs: 635 | anchor = dict(anchor1) 636 | pt1 = (anchor1["x"], anchor1["y"]) 637 | pt2 = (anchor2["x"], anchor2["y"]) 638 | anchor["x"], anchor["y"] = func(pt1, pt2) 639 | result.append(anchor) 640 | return result 641 | 642 | def _processMathTwoAnchors(anchors, factor, func): 643 | result = [] 644 | for anchor in anchors: 645 | anchor = dict(anchor) 646 | pt = (anchor["x"], anchor["y"]) 647 | anchor["x"], anchor["y"] = func(pt, factor) 648 | result.append(anchor) 649 | return result 650 | 651 | # components 652 | 653 | def _pairComponents(components1, components2): 654 | components1 = list(components1) 655 | components2 = list(components2) 656 | pairs = [] 657 | # align with matching identifiers 658 | removeFromComponents1 = [] 659 | for component1 in components1: 660 | baseGlyph = component1["baseGlyph"] 661 | identifier = component1["identifier"] 662 | match = None 663 | for component2 in components2: 664 | if component2["baseGlyph"] == baseGlyph and component2["identifier"] == identifier: 665 | match = component2 666 | break 667 | if match is not None: 668 | component2 = match 669 | removeFromComponents1.append(component1) 670 | components2.remove(component2) 671 | pairs.append((component1, component2)) 672 | for component1 in removeFromComponents1: 673 | components1.remove(component1) 674 | # align with index 675 | for component1 in components1: 676 | baseGlyph = component1["baseGlyph"] 677 | for component2 in components2: 678 | if component2["baseGlyph"] == baseGlyph: 679 | components2.remove(component2) 680 | pairs.append((component1, component2)) 681 | break 682 | return pairs 683 | 684 | def _processMathOneComponents(componentPairs, func): 685 | result = [] 686 | for component1, component2 in componentPairs: 687 | component = dict(component1) 688 | component["transformation"] = _processMathOneTransformation(component1["transformation"], component2["transformation"], func) 689 | result.append(component) 690 | return result 691 | 692 | def _processMathTwoComponents(components, factor, func, scaleComponentTransform=True): 693 | result = [] 694 | for component in components: 695 | component = dict(component) 696 | component["transformation"] = _processMathTwoTransformation( 697 | component["transformation"], factor, func, doScale=scaleComponentTransform 698 | ) 699 | result.append(component) 700 | return result 701 | 702 | # image 703 | 704 | _imageTransformationKeys = "xScale xyScale yxScale yScale xOffset yOffset".split(" ") 705 | _defaultImageTransformation = (1, 0, 0, 1, 0, 0) 706 | _defaultImageTransformationDict = {} 707 | for key, value in zip(_imageTransformationKeys, _defaultImageTransformation): 708 | _defaultImageTransformationDict[key] = value 709 | 710 | def _expandImage(image): 711 | if image is None: 712 | fileName = None 713 | transformation = _defaultImageTransformation 714 | color = None 715 | else: 716 | if hasattr(image, "naked"): 717 | image = image.naked() 718 | fileName = image["fileName"] 719 | color = image.get("color") 720 | transformation = tuple([ 721 | image.get(key, _defaultImageTransformationDict[key]) 722 | for key in _imageTransformationKeys 723 | ]) 724 | return dict(fileName=fileName, transformation=transformation, color=color) 725 | 726 | def _compressImage(image): 727 | fileName = image["fileName"] 728 | transformation = image["transformation"] 729 | color = image["color"] 730 | if fileName is None: 731 | return 732 | image = dict(fileName=fileName, color=color) 733 | for index, key in enumerate(_imageTransformationKeys): 734 | image[key] = transformation[index] 735 | return image 736 | 737 | def _pairImages(image1, image2): 738 | if image1["fileName"] != image2["fileName"]: 739 | return () 740 | return (image1, image2) 741 | 742 | def _processMathOneImage(imagePair, func): 743 | image1, image2 = imagePair 744 | fileName = image1["fileName"] 745 | color = image1["color"] 746 | transformation = _processMathOneTransformation(image1["transformation"], image2["transformation"], func) 747 | return dict(fileName=fileName, transformation=transformation, color=color) 748 | 749 | def _processMathTwoImage(image, factor, func): 750 | fileName = image["fileName"] 751 | color = image["color"] 752 | transformation = _processMathTwoTransformation(image["transformation"], factor, func) 753 | return dict(fileName=fileName, transformation=transformation, color=color) 754 | 755 | 756 | # transformations 757 | 758 | def _processMathOneTransformation(transformation1, transformation2, func): 759 | xScale1, xyScale1, yxScale1, yScale1, xOffset1, yOffset1 = transformation1 760 | xScale2, xyScale2, yxScale2, yScale2, xOffset2, yOffset2 = transformation2 761 | xScale, yScale = func((xScale1, yScale1), (xScale2, yScale2)) 762 | xyScale, yxScale = func((xyScale1, yxScale1), (xyScale2, yxScale2)) 763 | xOffset, yOffset = func((xOffset1, yOffset1), (xOffset2, yOffset2)) 764 | return (xScale, xyScale, yxScale, yScale, xOffset, yOffset) 765 | 766 | def _processMathTwoTransformation(transformation, factor, func, doScale=True): 767 | xScale, xyScale, yxScale, yScale, xOffset, yOffset = transformation 768 | if doScale: 769 | xScale, yScale = func((xScale, yScale), factor) 770 | xyScale, yxScale = func((xyScale, yxScale), factor) 771 | xOffset, yOffset = func((xOffset, yOffset), factor) 772 | return (xScale, xyScale, yxScale, yScale, xOffset, yOffset) 773 | 774 | 775 | # rounding 776 | 777 | def _roundContours(contours, digits=None): 778 | results = [] 779 | for contour in contours: 780 | contour = dict(contour) 781 | roundedPoints = [] 782 | for segmentType, pt, smooth, name, identifier in contour["points"]: 783 | roundedPt = (_roundNumber(pt[0],digits), _roundNumber(pt[1],digits)) 784 | roundedPoints.append((segmentType, roundedPt, smooth, name, identifier)) 785 | contour["points"] = roundedPoints 786 | results.append(contour) 787 | return results 788 | 789 | def _roundTransformation(transformation, digits=None): 790 | xScale, xyScale, yxScale, yScale, xOffset, yOffset = transformation 791 | return (xScale, xyScale, yxScale, yScale, _roundNumber(xOffset, digits), _roundNumber(yOffset, digits)) 792 | 793 | def _roundImage(image, digits=None): 794 | image = dict(image) 795 | fileName = image["fileName"] 796 | color = image["color"] 797 | transformation = _roundTransformation(image["transformation"], digits) 798 | return dict(fileName=fileName, transformation=transformation, color=color) 799 | 800 | def _roundComponents(components, digits=None): 801 | result = [] 802 | for component in components: 803 | component = dict(component) 804 | component["transformation"] = _roundTransformation(component["transformation"], digits) 805 | result.append(component) 806 | return result 807 | 808 | def _roundAnchors(anchors, digits=None): 809 | result = [] 810 | for anchor in anchors: 811 | anchor = dict(anchor) 812 | anchor["x"], anchor["y"] = _roundNumber(anchor["x"], digits), _roundNumber(anchor["y"], digits) 813 | result.append(anchor) 814 | return result 815 | 816 | 817 | if __name__ == "__main__": 818 | import sys 819 | import doctest 820 | sys.exit(doctest.testmod().failed) 821 | -------------------------------------------------------------------------------- /Lib/fontMath/mathGuideline.py: -------------------------------------------------------------------------------- 1 | from fontMath.mathFunctions import factorAngle, _roundNumber 2 | 3 | __all__ = [ 4 | "_expandGuideline", 5 | "_compressGuideline", 6 | "_pairGuidelines", 7 | "_processMathOneGuidelines", 8 | "_processMathTwoGuidelines", 9 | "_roundGuidelines" 10 | ] 11 | 12 | def _expandGuideline(guideline): 13 | guideline = dict(guideline) 14 | x = guideline.get("x") 15 | y = guideline.get("y") 16 | # horizontal 17 | if x is None: 18 | guideline["x"] = 0 19 | guideline["angle"] = 0 20 | # vertical 21 | elif y is None: 22 | guideline["y"] = 0 23 | guideline["angle"] = 90 24 | return guideline 25 | 26 | def _compressGuideline(guideline): 27 | guideline = dict(guideline) 28 | x = guideline["x"] 29 | y = guideline["y"] 30 | angle = guideline["angle"] 31 | # horizontal 32 | if x == 0 and angle in (0, 180): 33 | guideline["x"] = None 34 | guideline["angle"] = None 35 | # vertical 36 | elif y == 0 and angle in (90, 270): 37 | guideline["y"] = None 38 | guideline["angle"] = None 39 | return guideline 40 | 41 | def _pairGuidelines(guidelines1, guidelines2): 42 | guidelines1 = list(guidelines1) 43 | guidelines2 = list(guidelines2) 44 | pairs = [] 45 | # name + identifier + (x, y, angle) 46 | _findPair(guidelines1, guidelines2, pairs, ("name", "identifier", "x", "y", "angle")) 47 | # name + identifier matches 48 | _findPair(guidelines1, guidelines2, pairs, ("name", "identifier")) 49 | # name + (x, y, angle) 50 | _findPair(guidelines1, guidelines2, pairs, ("name", "x", "y", "angle")) 51 | # identifier + (x, y, angle) 52 | _findPair(guidelines1, guidelines2, pairs, ("identifier", "x", "y", "angle")) 53 | # name matches 54 | if guidelines1 and guidelines2: 55 | _findPair(guidelines1, guidelines2, pairs, ("name",)) 56 | # identifier matches 57 | if guidelines1 and guidelines2: 58 | _findPair(guidelines1, guidelines2, pairs, ("identifier",)) 59 | # done 60 | return pairs 61 | 62 | def _findPair(guidelines1, guidelines2, pairs, attrs): 63 | removeFromGuidelines1 = [] 64 | for guideline1 in guidelines1: 65 | match = None 66 | for guideline2 in guidelines2: 67 | attrMatch = False not in [guideline1.get(attr) == guideline2.get(attr) for attr in attrs] 68 | if attrMatch: 69 | match = guideline2 70 | break 71 | if match is not None: 72 | guideline2 = match 73 | removeFromGuidelines1.append(guideline1) 74 | guidelines2.remove(guideline2) 75 | pairs.append((guideline1, guideline2)) 76 | for removeGuide in removeFromGuidelines1: 77 | guidelines1.remove(removeGuide) 78 | 79 | def _processMathOneGuidelines(guidelinePairs, ptFunc, func): 80 | result = [] 81 | for guideline1, guideline2 in guidelinePairs: 82 | guideline = dict(guideline1) 83 | pt1 = (guideline1["x"], guideline1["y"]) 84 | pt2 = (guideline2["x"], guideline2["y"]) 85 | guideline["x"], guideline["y"] = ptFunc(pt1, pt2) 86 | angle1 = guideline1["angle"] 87 | angle2 = guideline2["angle"] 88 | guideline["angle"] = func(angle1, angle2) % 360 89 | result.append(guideline) 90 | return result 91 | 92 | def _processMathTwoGuidelines(guidelines, factor, func): 93 | result = [] 94 | for guideline in guidelines: 95 | guideline = dict(guideline) 96 | guideline["x"] = func(guideline["x"], factor[0]) 97 | guideline["y"] = func(guideline["y"], factor[1]) 98 | angle = guideline["angle"] 99 | guideline["angle"] = factorAngle(angle, factor, func) % 360 100 | result.append(guideline) 101 | return result 102 | 103 | def _roundGuidelines(guidelines, digits=None): 104 | results = [] 105 | for guideline in guidelines: 106 | guideline = dict(guideline) 107 | guideline['x'] = _roundNumber(guideline['x'], digits) 108 | guideline['y'] = _roundNumber(guideline['y'], digits) 109 | results.append(guideline) 110 | return results 111 | 112 | 113 | 114 | if __name__ == "__main__": 115 | import sys 116 | import doctest 117 | sys.exit(doctest.testmod().failed) 118 | -------------------------------------------------------------------------------- /Lib/fontMath/mathInfo.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import 2 | from fontMath.mathFunctions import ( 3 | add, addPt, div, factorAngle, mul, _roundNumber, sub, subPt, round2) 4 | from fontMath.mathGuideline import ( 5 | _expandGuideline, _pairGuidelines, _processMathOneGuidelines, 6 | _processMathTwoGuidelines, _roundGuidelines) 7 | 8 | 9 | class MathInfo(object): 10 | 11 | def __init__(self, infoObject): 12 | for attr in _infoAttrs.keys(): 13 | if hasattr(infoObject, attr): 14 | setattr(self, attr, getattr(infoObject, attr)) 15 | if isinstance(infoObject, MathInfo): 16 | self.guidelines = [dict(guideline) for guideline in infoObject.guidelines] 17 | elif infoObject.guidelines is not None: 18 | self.guidelines = [_expandGuideline(guideline) for guideline in infoObject.guidelines] 19 | else: 20 | self.guidelines = [] 21 | 22 | # ---- 23 | # Copy 24 | # ---- 25 | 26 | def copy(self): 27 | copied = MathInfo(self) 28 | return copied 29 | 30 | # ---- 31 | # Math 32 | # ---- 33 | 34 | # math with other info 35 | 36 | def __add__(self, otherInfo): 37 | copiedInfo = self.copy() 38 | self._processMathOne(copiedInfo, otherInfo, addPt, add) 39 | return copiedInfo 40 | 41 | def __sub__(self, otherInfo): 42 | copiedInfo = self.copy() 43 | self._processMathOne(copiedInfo, otherInfo, subPt, sub) 44 | return copiedInfo 45 | 46 | def _processMathOne(self, copiedInfo, otherInfo, ptFunc, func): 47 | # basic attributes 48 | for attr in _infoAttrs.keys(): 49 | a = None 50 | b = None 51 | v = None 52 | if hasattr(copiedInfo, attr): 53 | a = getattr(copiedInfo, attr) 54 | if hasattr(otherInfo, attr): 55 | b = getattr(otherInfo, attr) 56 | if a is not None and b is not None: 57 | if isinstance(a, (list, tuple)): 58 | v = self._processMathOneNumberList(a, b, func) 59 | else: 60 | v = self._processMathOneNumber(a, b, func) 61 | # when one of the terms is undefined, we treat addition and subtraction 62 | # differently... 63 | # https://github.com/robotools/fontMath/issues/175 64 | # https://github.com/robotools/fontMath/issues/136 65 | elif a is not None and b is None: 66 | if func == add: 67 | v = a 68 | else: 69 | v = None if attr in _numberListAttrs else 0 70 | elif b is not None and a is None: 71 | if func is add: 72 | v = b 73 | else: 74 | v = None if attr in _numberListAttrs else 0 75 | setattr(copiedInfo, attr, v) 76 | # special attributes 77 | self._processPostscriptWeightName(copiedInfo) 78 | # guidelines 79 | copiedInfo.guidelines = [] 80 | if self.guidelines: 81 | guidelinePairs = _pairGuidelines(self.guidelines, otherInfo.guidelines) 82 | copiedInfo.guidelines = _processMathOneGuidelines(guidelinePairs, ptFunc, func) 83 | 84 | def _processMathOneNumber(self, a, b, func): 85 | return func(a, b) 86 | 87 | def _processMathOneNumberList(self, a, b, func): 88 | if len(a) != len(b): 89 | return None 90 | v = [] 91 | for index, aItem in enumerate(a): 92 | bItem = b[index] 93 | v.append(func(aItem, bItem)) 94 | return v 95 | 96 | # math with factor 97 | 98 | def __mul__(self, factor): 99 | if not isinstance(factor, tuple): 100 | factor = (factor, factor) 101 | copiedInfo = self.copy() 102 | self._processMathTwo(copiedInfo, factor, mul) 103 | return copiedInfo 104 | 105 | __rmul__ = __mul__ 106 | 107 | def __div__(self, factor): 108 | if not isinstance(factor, tuple): 109 | factor = (factor, factor) 110 | copiedInfo = self.copy() 111 | self._processMathTwo(copiedInfo, factor, div) 112 | return copiedInfo 113 | 114 | __truediv__ = __div__ 115 | 116 | __rdiv__ = __div__ 117 | 118 | __rtruediv__ = __rdiv__ 119 | 120 | def _processMathTwo(self, copiedInfo, factor, func): 121 | # basic attributes 122 | for attr, (formatter, factorIndex) in _infoAttrs.items(): 123 | if hasattr(copiedInfo, attr): 124 | v = getattr(copiedInfo, attr) 125 | if v is not None and factor is not None: 126 | if factorIndex == 3: 127 | v = self._processMathTwoAngle(v, factor, func) 128 | else: 129 | if isinstance(v, (list, tuple)): 130 | v = self._processMathTwoNumberList(v, factor[factorIndex], func) 131 | else: 132 | v = self._processMathTwoNumber(v, factor[factorIndex], func) 133 | else: 134 | v = None 135 | setattr(copiedInfo, attr, v) 136 | # special attributes 137 | self._processPostscriptWeightName(copiedInfo) 138 | # guidelines 139 | copiedInfo.guidelines = [] 140 | if self.guidelines: 141 | copiedInfo.guidelines = _processMathTwoGuidelines(self.guidelines, factor, func) 142 | 143 | def _processMathTwoNumber(self, v, factor, func): 144 | return func(v, factor) 145 | 146 | def _processMathTwoNumberList(self, v, factor, func): 147 | return [func(i, factor) for i in v] 148 | 149 | def _processMathTwoAngle(self, angle, factor, func): 150 | return factorAngle(angle, factor, func) 151 | 152 | # special attributes 153 | 154 | def _processPostscriptWeightName(self, copiedInfo): 155 | # handle postscriptWeightName by taking the value 156 | # of openTypeOS2WeightClass and getting the closest 157 | # value from the OS/2 specification. 158 | name = None 159 | if hasattr(copiedInfo, "openTypeOS2WeightClass") and copiedInfo.openTypeOS2WeightClass is not None: 160 | v = copiedInfo.openTypeOS2WeightClass 161 | # here we use Python 2 rounding (i.e. away from 0) instead of Python 3: 162 | # e.g. 150 -> 200 and 250 -> 300, instead of 150 -> 200 and 250 -> 200 163 | v = int(round2(v, -2)) 164 | if v < 100: 165 | v = 100 166 | elif v > 900: 167 | v = 900 168 | name = _postscriptWeightNameOptions[v] 169 | copiedInfo.postscriptWeightName = name 170 | 171 | # ---------- 172 | # More math 173 | # ---------- 174 | 175 | def round(self, digits=None): 176 | excludeFromRounding = ['postscriptBlueScale', 'italicAngle'] 177 | copiedInfo = self.copy() 178 | # basic attributes 179 | for attr, (formatter, factorIndex) in _infoAttrs.items(): 180 | if attr in excludeFromRounding: 181 | continue 182 | if hasattr(copiedInfo, attr): 183 | v = getattr(copiedInfo, attr) 184 | if v is not None: 185 | if factorIndex == 3: 186 | v = _roundNumber(v) 187 | else: 188 | if isinstance(v, (list, tuple)): 189 | v = [_roundNumber(a, digits) for a in v] 190 | else: 191 | v = _roundNumber(v, digits) 192 | else: 193 | v = None 194 | setattr(copiedInfo, attr, v) 195 | # special attributes 196 | self._processPostscriptWeightName(copiedInfo) 197 | # guidelines 198 | copiedInfo.guidelines = [] 199 | if self.guidelines: 200 | copiedInfo.guidelines = _roundGuidelines(self.guidelines, digits) 201 | return copiedInfo 202 | 203 | # ---------- 204 | # Extraction 205 | # ---------- 206 | 207 | def extractInfo(self, otherInfoObject): 208 | """ 209 | >>> from fontMath.test.test_mathInfo import _TestInfoObject, _testData 210 | >>> from fontMath.mathFunctions import _roundNumber 211 | >>> info1 = MathInfo(_TestInfoObject()) 212 | >>> info2 = info1 * 2.5 213 | >>> info3 = _TestInfoObject() 214 | >>> info2.extractInfo(info3) 215 | >>> written = {} 216 | >>> expected = {} 217 | >>> for attr, value in _testData.items(): 218 | ... if value is None: 219 | ... continue 220 | ... written[attr] = getattr(info2, attr) 221 | ... if isinstance(value, list): 222 | ... expectedValue = [_roundNumber(v * 2.5) for v in value] 223 | ... elif isinstance(value, int): 224 | ... expectedValue = _roundNumber(value * 2.5) 225 | ... else: 226 | ... expectedValue = value * 2.5 227 | ... expected[attr] = expectedValue 228 | >>> sorted(expected) == sorted(written) 229 | True 230 | """ 231 | for attr, (formatter, factorIndex) in _infoAttrs.items(): 232 | if hasattr(self, attr): 233 | v = getattr(self, attr) 234 | if v is not None: 235 | if formatter is not None: 236 | v = formatter(v) 237 | setattr(otherInfoObject, attr, v) 238 | if hasattr(self, "postscriptWeightName"): 239 | otherInfoObject.postscriptWeightName = self.postscriptWeightName 240 | 241 | # ------- 242 | # Sorting 243 | # ------- 244 | 245 | def __lt__(self, other): 246 | if set(self.__dict__.keys()) < set(other.__dict__.keys()): 247 | return True 248 | elif set(self.__dict__.keys()) > set(other.__dict__.keys()): 249 | return False 250 | for attr, value in self.__dict__.items(): 251 | other_value = getattr(other, attr) 252 | if value is not None and other_value is not None: 253 | # guidelines is a list of dicts 254 | if attr == "guidelines": 255 | if len(value) < len(other_value): 256 | return True 257 | elif len(value) > len(other_value): 258 | return False 259 | for i, guide in enumerate(value): 260 | if set(guide) < set(other_value[i]): 261 | return True 262 | elif set(guide) > set(other_value[i]): 263 | return False 264 | for key, val in guide.items(): 265 | if key in other_value[i]: 266 | if val < other_value[i][key]: 267 | return True 268 | elif value < other_value: 269 | return True 270 | return False 271 | 272 | def __eq__(self, other): 273 | if set(self.__dict__.keys()) != set(other.__dict__.keys()): 274 | return False 275 | for attr, value in self.__dict__.items(): 276 | if hasattr(other, attr) and value != getattr(other, attr): 277 | return False 278 | return True 279 | 280 | 281 | # ---------- 282 | # Formatters 283 | # ---------- 284 | 285 | def _numberFormatter(value): 286 | v = int(value) 287 | if v == value: 288 | return v 289 | return value 290 | 291 | def _integerFormatter(value): 292 | return _roundNumber(value) 293 | 294 | def _floatFormatter(value): 295 | return float(value) 296 | 297 | def _nonNegativeNumberFormatter(value): 298 | """ 299 | >>> _nonNegativeNumberFormatter(-10) 300 | 0 301 | """ 302 | if value < 0: 303 | return 0 304 | return value 305 | 306 | def _nonNegativeIntegerFormatter(value): 307 | value = _integerFormatter(value) 308 | if value < 0: 309 | return 0 310 | return value 311 | 312 | def _integerListFormatter(value): 313 | """ 314 | >>> _integerListFormatter([.9, 40.3, 16.0001]) 315 | [1, 40, 16] 316 | """ 317 | return [_integerFormatter(v) for v in value] 318 | 319 | def _numberListFormatter(value): 320 | return [_numberFormatter(v) for v in value] 321 | 322 | def _openTypeOS2WidthClassFormatter(value): 323 | """ 324 | >>> _openTypeOS2WidthClassFormatter(-2) 325 | 1 326 | >>> _openTypeOS2WidthClassFormatter(0) 327 | 1 328 | >>> _openTypeOS2WidthClassFormatter(5.4) 329 | 5 330 | >>> _openTypeOS2WidthClassFormatter(9.6) 331 | 9 332 | >>> _openTypeOS2WidthClassFormatter(12) 333 | 9 334 | """ 335 | value = int(round2(value)) 336 | if value > 9: 337 | value = 9 338 | elif value < 1: 339 | value = 1 340 | return value 341 | 342 | def _openTypeOS2WeightClassFormatter(value): 343 | """ 344 | >>> _openTypeOS2WeightClassFormatter(-20) 345 | 0 346 | >>> _openTypeOS2WeightClassFormatter(0) 347 | 0 348 | >>> _openTypeOS2WeightClassFormatter(50.4) 349 | 50 350 | >>> _openTypeOS2WeightClassFormatter(90.6) 351 | 91 352 | >>> _openTypeOS2WeightClassFormatter(120) 353 | 120 354 | """ 355 | value = _roundNumber(value) 356 | if value < 0: 357 | value = 0 358 | return value 359 | 360 | _infoAttrs = dict( 361 | # these are structured as: 362 | # attribute name = (formatter function, factor direction) 363 | # where factor direction 0 = x, 1 = y and 3 = x, y (for angles) 364 | 365 | unitsPerEm=(_nonNegativeNumberFormatter, 1), 366 | descender=(_numberFormatter, 1), 367 | xHeight=(_numberFormatter, 1), 368 | capHeight=(_numberFormatter, 1), 369 | ascender=(_numberFormatter, 1), 370 | italicAngle=(_numberFormatter, 3), 371 | 372 | openTypeHeadLowestRecPPEM=(_nonNegativeIntegerFormatter, 1), 373 | 374 | openTypeHheaAscender=(_integerFormatter, 1), 375 | openTypeHheaDescender=(_integerFormatter, 1), 376 | openTypeHheaLineGap=(_integerFormatter, 1), 377 | openTypeHheaCaretSlopeRise=(_integerFormatter, 1), 378 | openTypeHheaCaretSlopeRun=(_integerFormatter, 1), 379 | openTypeHheaCaretOffset=(_integerFormatter, 1), 380 | 381 | openTypeOS2WidthClass=(_openTypeOS2WidthClassFormatter, 0), 382 | openTypeOS2WeightClass=(_openTypeOS2WeightClassFormatter, 0), 383 | openTypeOS2TypoAscender=(_integerFormatter, 1), 384 | openTypeOS2TypoDescender=(_integerFormatter, 1), 385 | openTypeOS2TypoLineGap=(_integerFormatter, 1), 386 | openTypeOS2WinAscent=(_nonNegativeIntegerFormatter, 1), 387 | openTypeOS2WinDescent=(_nonNegativeIntegerFormatter, 1), 388 | openTypeOS2SubscriptXSize=(_integerFormatter, 0), 389 | openTypeOS2SubscriptYSize=(_integerFormatter, 1), 390 | openTypeOS2SubscriptXOffset=(_integerFormatter, 0), 391 | openTypeOS2SubscriptYOffset=(_integerFormatter, 1), 392 | openTypeOS2SuperscriptXSize=(_integerFormatter, 0), 393 | openTypeOS2SuperscriptYSize=(_integerFormatter, 1), 394 | openTypeOS2SuperscriptXOffset=(_integerFormatter, 0), 395 | openTypeOS2SuperscriptYOffset=(_integerFormatter, 1), 396 | openTypeOS2StrikeoutSize=(_integerFormatter, 1), 397 | openTypeOS2StrikeoutPosition=(_integerFormatter, 1), 398 | 399 | openTypeVheaVertTypoAscender=(_integerFormatter, 1), 400 | openTypeVheaVertTypoDescender=(_integerFormatter, 1), 401 | openTypeVheaVertTypoLineGap=(_integerFormatter, 1), 402 | openTypeVheaCaretSlopeRise=(_integerFormatter, 1), 403 | openTypeVheaCaretSlopeRun=(_integerFormatter, 1), 404 | openTypeVheaCaretOffset=(_integerFormatter, 1), 405 | 406 | postscriptSlantAngle=(_numberFormatter, 3), 407 | postscriptUnderlineThickness=(_numberFormatter, 1), 408 | postscriptUnderlinePosition=(_numberFormatter, 1), 409 | postscriptBlueValues=(_numberListFormatter, 1), 410 | postscriptOtherBlues=(_numberListFormatter, 1), 411 | postscriptFamilyBlues=(_numberListFormatter, 1), 412 | postscriptFamilyOtherBlues=(_numberListFormatter, 1), 413 | postscriptStemSnapH=(_numberListFormatter, 0), 414 | postscriptStemSnapV=(_numberListFormatter, 1), 415 | postscriptBlueFuzz=(_numberFormatter, 1), 416 | postscriptBlueShift=(_numberFormatter, 1), 417 | postscriptBlueScale=(_floatFormatter, 1), 418 | postscriptDefaultWidthX=(_numberFormatter, 0), 419 | postscriptNominalWidthX=(_numberFormatter, 0), 420 | # this will be handled in a special way 421 | # postscriptWeightName=unicode 422 | ) 423 | 424 | _numberListAttrs = { 425 | attr 426 | for attr, (formatter, _) in _infoAttrs.items() 427 | if formatter is _numberListFormatter 428 | } 429 | 430 | _postscriptWeightNameOptions = { 431 | 100 : "Thin", 432 | 200 : "Extra-light", 433 | 300 : "Light", 434 | 400 : "Normal", 435 | 500 : "Medium", 436 | 600 : "Semi-bold", 437 | 700 : "Bold", 438 | 800 : "Extra-bold", 439 | 900 : "Black" 440 | } 441 | 442 | if __name__ == "__main__": 443 | import sys 444 | import doctest 445 | sys.exit(doctest.testmod().failed) 446 | -------------------------------------------------------------------------------- /Lib/fontMath/mathKerning.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import 2 | from copy import deepcopy 3 | from fontMath.mathFunctions import add, sub, mul, div, round2 4 | 5 | """ 6 | An object that serves kerning data from a 7 | class kerning dictionary. 8 | 9 | It scans a group dictionary and stores 10 | a mapping of glyph to group relationships. 11 | this map is then used to lookup kerning values. 12 | """ 13 | 14 | 15 | side1Prefix = "public.kern1." 16 | side2Prefix = "public.kern2." 17 | 18 | 19 | class MathKerning(object): 20 | 21 | def __init__(self, kerning=None, groups=None): 22 | if kerning is None: 23 | kerning = {} 24 | if groups is None: 25 | groups = {} 26 | self.update(kerning) 27 | self.updateGroups(groups) 28 | 29 | # ------- 30 | # Loading 31 | # ------- 32 | 33 | def update(self, kerning): 34 | self._kerning = dict(kerning) 35 | 36 | def updateGroups(self, groups): 37 | self._groups = {} 38 | self._side1GroupMap = {} 39 | self._side2GroupMap = {} 40 | self._side1Groups = {} 41 | self._side2Groups = {} 42 | for groupName, glyphList in groups.items(): 43 | if groupName.startswith(side1Prefix): 44 | self._groups[groupName] = list(glyphList) 45 | self._side1Groups[groupName] = glyphList 46 | for glyphName in glyphList: 47 | self._side1GroupMap[glyphName] = groupName 48 | elif groupName.startswith(side2Prefix): 49 | self._groups[groupName] = list(glyphList) 50 | self._side2Groups[groupName] = glyphList 51 | for glyphName in glyphList: 52 | self._side2GroupMap[glyphName] = groupName 53 | 54 | def addTo(self, value): 55 | for k, v in self._kerning.items(): 56 | self._kerning[k] = v + value 57 | 58 | # ------------- 59 | # dict Behavior 60 | # ------------- 61 | 62 | def keys(self): 63 | return self._kerning.keys() 64 | 65 | def values(self): 66 | return self._kerning.values() 67 | 68 | def items(self): 69 | return self._kerning.items() 70 | 71 | def groups(self): 72 | return deepcopy(self._groups) 73 | 74 | def __contains__(self, pair): 75 | return pair in self._kerning 76 | 77 | def __getitem__(self, pair): 78 | if pair in self._kerning: 79 | return self._kerning[pair] 80 | side1, side2 = pair 81 | if side1.startswith(side1Prefix): 82 | side1Group = side1 83 | side1 = None 84 | else: 85 | side1Group = self._side1GroupMap.get(side1) 86 | if side2.startswith(side2Prefix): 87 | side2Group = side2 88 | side2 = None 89 | else: 90 | side2Group = self._side2GroupMap.get(side2) 91 | if (side1Group, side2) in self._kerning: 92 | return self._kerning[side1Group, side2] 93 | elif (side1, side2Group) in self._kerning: 94 | return self._kerning[side1, side2Group] 95 | elif (side1Group, side2Group) in self._kerning: 96 | return self._kerning[side1Group, side2Group] 97 | else: 98 | return 0 99 | 100 | def get(self, pair): 101 | v = self[pair] 102 | return v 103 | 104 | # --------- 105 | # Pair Type 106 | # --------- 107 | 108 | def guessPairType(self, pair): 109 | side1, side2 = pair 110 | if side1.startswith(side1Prefix): 111 | side1Group = side1 112 | else: 113 | side1Group = self._side1GroupMap.get(side1) 114 | if side2.startswith(side2Prefix): 115 | side2Group = side2 116 | else: 117 | side2Group = self._side2GroupMap.get(side2) 118 | side1Type = side2Type = "glyph" 119 | if pair in self: 120 | if side1 == side1Group: 121 | side1Type = "group" 122 | elif side1Group is not None: 123 | side1Type = "exception" 124 | if side2 == side2Group: 125 | side2Type = "group" 126 | elif side2Group is not None: 127 | side2Type = "exception" 128 | elif (side1Group, side2) in self: 129 | side1Type = "group" 130 | if side2Group is not None: 131 | if side2 != side2Group: 132 | side2Type = "exception" 133 | elif (side1, side2Group) in self: 134 | side2Type = "group" 135 | if side1Group is not None: 136 | if side1 != side1Group: 137 | side1Type = "exception" 138 | else: 139 | if side1Group is not None: 140 | side1Type = "group" 141 | if side2Group is not None: 142 | side2Type = "group" 143 | return side1Type, side2Type 144 | 145 | # ---- 146 | # Copy 147 | # ---- 148 | 149 | def copy(self): 150 | k = MathKerning(self._kerning, self._groups) 151 | return k 152 | 153 | # ---- 154 | # Math 155 | # ---- 156 | 157 | # math with other kerning 158 | 159 | def __add__(self, other): 160 | k = self._processMathOne(other, add) 161 | k.cleanup() 162 | return k 163 | 164 | def __sub__(self, other): 165 | k = self._processMathOne(other, sub) 166 | k.cleanup() 167 | return k 168 | 169 | def _processMathOne(self, other, funct): 170 | comboPairs = set(self._kerning.keys()) | set(other._kerning.keys()) 171 | kerning = dict.fromkeys(comboPairs, None) 172 | for k in comboPairs: 173 | v1 = self.get(k) 174 | v2 = other.get(k) 175 | v = funct(v1, v2) 176 | kerning[k] = v 177 | g1 = self.groups() 178 | g2 = other.groups() 179 | if g1 == g2 or not g1 or not g2: 180 | groups = g1 or g2 181 | else: 182 | comboGroups = set(g1.keys()) | set(g2.keys()) 183 | groups = dict.fromkeys(comboGroups, None) 184 | for groupName in comboGroups: 185 | s1 = set(g1.get(groupName, [])) 186 | s2 = set(g2.get(groupName, [])) 187 | groups[groupName] = sorted(list(s1 | s2)) 188 | ks = MathKerning(kerning, groups) 189 | return ks 190 | 191 | # math with factor 192 | 193 | def __mul__(self, factor): 194 | if isinstance(factor, tuple): 195 | factor = factor[0] 196 | k = self._processMathTwo(factor, mul) 197 | k.cleanup() 198 | return k 199 | 200 | def __rmul__(self, factor): 201 | if isinstance(factor, tuple): 202 | factor = factor[0] 203 | k = self._processMathTwo(factor, mul) 204 | k.cleanup() 205 | return k 206 | 207 | def __div__(self, factor): 208 | if isinstance(factor, tuple): 209 | factor = factor[0] 210 | k = self._processMathTwo(factor, div) 211 | k.cleanup() 212 | return k 213 | 214 | __truediv__ = __div__ 215 | 216 | def _processMathTwo(self, factor, funct): 217 | kerning = deepcopy(self._kerning) 218 | for k, v in self._kerning.items(): 219 | v = funct(v, factor) 220 | kerning[k] = v 221 | ks = MathKerning(kerning, self._groups) 222 | return ks 223 | 224 | # --------- 225 | # More math 226 | # --------- 227 | 228 | def round(self, multiple=1): 229 | multiple = float(multiple) 230 | for k, v in self._kerning.items(): 231 | self._kerning[k] = int(round2(int(round2(v / multiple)) * multiple)) 232 | 233 | # ------- 234 | # Cleanup 235 | # ------- 236 | 237 | def cleanup(self): 238 | for (side1, side2), v in list(self._kerning.items()): 239 | if int(v) == v: 240 | v = int(v) 241 | self._kerning[side1, side2] = v 242 | if v == 0: 243 | side1Type, side2Type = self.guessPairType((side1, side2)) 244 | if side1Type != "exception" and side2Type != "exception": 245 | del self._kerning[side1, side2] 246 | 247 | # ---------- 248 | # Extraction 249 | # ---------- 250 | 251 | def extractKerning(self, font): 252 | font.kerning.clear() 253 | font.kerning.update(self._kerning) 254 | font.groups.update(self.groups()) 255 | 256 | # ------- 257 | # Sorting 258 | # ------- 259 | def _isLessish(self, first, second): 260 | if len(first) < len(second): 261 | return True 262 | elif len(first) > len(second): 263 | return False 264 | 265 | first_keys = sorted(first) 266 | second_keys = sorted(second) 267 | for i, key in enumerate(first_keys): 268 | if first_keys[i] < second_keys[i]: 269 | return True 270 | elif first_keys[i] > second_keys[i]: 271 | return False 272 | elif first[key] < second[key]: 273 | return True 274 | return None 275 | 276 | def __lt__(self, other): 277 | if other is None: 278 | return False 279 | 280 | lessish = self._isLessish(self._kerning, other._kerning) 281 | if lessish is not None: 282 | return lessish 283 | 284 | lessish = self._isLessish(self._side1Groups, other._side1Groups) 285 | if lessish is not None: 286 | return lessish 287 | 288 | lessish = self._isLessish(self._side2Groups, other._side2Groups) 289 | if lessish is not None: 290 | return lessish 291 | 292 | return False 293 | 294 | def __eq__(self, other): 295 | if self._kerning != other._kerning: 296 | return False 297 | if self._side1Groups != other._side1Groups: 298 | return False 299 | if self._side2Groups != other._side2Groups: 300 | return False 301 | return True 302 | 303 | 304 | if __name__ == "__main__": 305 | import sys 306 | import doctest 307 | sys.exit(doctest.testmod().failed) 308 | -------------------------------------------------------------------------------- /Lib/fontMath/mathTransform.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import math 3 | from fontTools.misc.transform import Transform 4 | 5 | """ 6 | 7 | This is a more sophisticated approach to performing math on transformation matrices. 8 | Traditionally glyphMath applies the math straight to the elements in the matrix. 9 | By decomposing the matrix into offset, scale and rotation factors, the interpoations 10 | are much more natural. Or more intuitive. 11 | 12 | This could help in complex glyphs in which the rotation of a component plays am important role. 13 | 14 | This MathTransform object itself has its own interpolation method. But in order to be able 15 | to participate in (for instance) superpolator math, it is necessary to keep the 16 | offset, scale and rotation decomposed for more than one math operation. 17 | So, MathTransform decomposes the matrix, ShallowTransform carries it through the math, 18 | then MathTransform is used again to compose the new matrix. If you don't need to math with 19 | the transformation object itself, the MathTransform object is fine. 20 | 21 | MathTransform by Frederik Berlaen 22 | 23 | Transformation decomposition algorithm from 24 | http://dojotoolkit.org/reference-guide/1.9/dojox/gfx.html#decompose-js 25 | http://dojotoolkit.org/license 26 | 27 | 28 | """ 29 | 30 | def matrixToMathTransform(matrix): 31 | """ Take a 6-tuple and return a ShallowTransform object.""" 32 | if isinstance(matrix, ShallowTransform): 33 | return matrix 34 | off, scl, rot = MathTransform(matrix).decompose() 35 | return ShallowTransform(off, scl, rot) 36 | 37 | def mathTransformToMatrix(mathTransform): 38 | """ Take a ShallowTransform object and return a 6-tuple. """ 39 | m = MathTransform().compose(mathTransform.offset, mathTransform.scale, mathTransform.rotation) 40 | return tuple(m) 41 | 42 | class ShallowTransform(object): 43 | """ A shallow math container for offset, scale and rotation. """ 44 | def __init__(self, offset, scale, rotation): 45 | self.offset = offset 46 | self.scale = scale 47 | self.rotation = rotation 48 | 49 | def __repr__(self): 50 | return ""%(self.offset[0], self.offset[1], self.scale[0], self.scale[1], self.rotation[0], self.rotation[1]) 51 | 52 | def __add__(self, other): 53 | newOffset = self.offset[0]+other.offset[0],self.offset[1]+other.offset[1] 54 | newScale = self.scale[0]+other.scale[0],self.scale[1]+other.scale[1] 55 | newRotation = self.rotation[0]+other.rotation[0],self.rotation[1]+other.rotation[1] 56 | return self.__class__(newOffset, newScale, newRotation) 57 | 58 | def __sub__(self, other): 59 | newOffset = self.offset[0]-other.offset[0],self.offset[1]-other.offset[1] 60 | newScale = self.scale[0]-other.scale[0],self.scale[1]-other.scale[1] 61 | 62 | newRotation = self.rotation[0]-other.rotation[0],self.rotation[1]-other.rotation[1] 63 | return self.__class__(newOffset, newScale, newRotation) 64 | 65 | def __mul__(self, factor): 66 | if isinstance(factor, (int, float)): 67 | fx = fy = float(factor) 68 | else: 69 | fx, fy = float(factor[0]), float(factor[1]) 70 | newOffset = self.offset[0]*fx,self.offset[1]*fy 71 | newScale = self.scale[0]*fx,self.scale[1]*fy 72 | newRotation = self.rotation[0]*fx,self.rotation[1]*fy 73 | return self.__class__(newOffset, newScale, newRotation) 74 | 75 | __rmul__ = __mul__ 76 | 77 | def __truediv__(self, factor): 78 | """ XXX why not __div__ ?""" 79 | if isinstance(factor, (int, float)): 80 | fx = fy = float(factor) 81 | else: 82 | fx, fy = float(factor) 83 | if fx==0 or fy==0: 84 | raise ZeroDivisionError((fx, fy)) 85 | newOffset = self.offset[0]/fx,self.offset[1]/fy 86 | newScale = self.scale[0]/fx,self.scale[1]/fy 87 | newRotation = self.rotation[0]/fx,self.rotation[1]/fy 88 | return self.__class__(newOffset, newScale, newRotation) 89 | 90 | def asTuple(self): 91 | m = MathTransform().compose(self.offset, self.scale, self.rotation) 92 | return tuple(m) 93 | 94 | 95 | 96 | class MathTransform(object): 97 | """ A Transform object that can compose and decompose the matrix into offset, scale and rotation.""" 98 | transformClass = Transform 99 | 100 | def __init__(self, *matrixes): 101 | matrix = self.transformClass() 102 | if matrixes: 103 | if isinstance(matrixes[0], (int, float)): 104 | matrixes = [matrixes] 105 | for m in matrixes: 106 | matrix = matrix.transform(m) 107 | self.matrix = matrix 108 | 109 | def _get_matrix(self): 110 | return (self.xx, self.xy, self.yx, self.yy, self.dx, self.dy) 111 | 112 | def _set_matrix(self, matrix): 113 | self.xx, self.xy, self.yx, self.yy, self.dx, self.dy = matrix 114 | 115 | matrix = property(_get_matrix, _set_matrix) 116 | 117 | def __repr__(self): 118 | return "< %.8f %.8f %.8f %.8f %.8f %.8f >" % (self.xx, self.xy, self.yx, self.yy, self.dx, self.dy) 119 | 120 | def __len__(self): 121 | return 6 122 | 123 | def __getitem__(self, index): 124 | return self.matrix[index] 125 | 126 | def __getslice__(self, i, j): 127 | return self.matrix[i:j] 128 | 129 | def __eq__(self, other): 130 | return str(self) == str(other) 131 | 132 | ## transformations 133 | 134 | def translate(self, x=0, y=0): 135 | return self.__class__(self.transformClass(*self.matrix).translate(x, y)) 136 | 137 | def scale(self, x=1, y=None): 138 | return self.__class__(self.transformClass(*self.matrix).scale(x, y)) 139 | 140 | def rotate(self, angle): 141 | return self.__class__(self.transformClass(*self.matrix).rotate(angle)) 142 | 143 | def rotateDegrees(self, angle): 144 | return self.rotate(math.radians(angle)) 145 | 146 | def skew(self, x=0, y=0): 147 | return self.__class__(self.transformClass(*self.matrix).skew(x, y)) 148 | 149 | def skewDegrees(self, x=0, y=0): 150 | return self.skew(math.radians(x), math.radians(y)) 151 | 152 | def transform(self, other): 153 | return self.__class__(self.transformClass(*self.matrix).transform(other)) 154 | 155 | def reverseTransform(self, other): 156 | return self.__class__(self.transformClass(*self.matrix).reverseTransform(other)) 157 | 158 | def inverse(self): 159 | return self.__class__(self.transformClass(*self.matrix).inverse()) 160 | 161 | def copy(self): 162 | return self.__class__(self.matrix) 163 | ## tools 164 | 165 | def scaleSign(self): 166 | if self.xx * self.yy < 0 or self.xy * self.yx > 0: 167 | return -1 168 | return 1 169 | 170 | def eq(self, a, b): 171 | return abs(a - b) <= 1e-6 * (abs(a) + abs(b)) 172 | 173 | def calcFromValues(self, r1, m1, r2, m2): 174 | m1 = abs(m1) 175 | m2 = abs(m2) 176 | return (m1 * r1 + m2 * r2) / (m1 + m2) 177 | 178 | def transpose(self): 179 | return self.__class__(self.xx, self.yx, self.xy, self.yy, 0, 0) 180 | 181 | def decompose(self): 182 | self.translateX = self.dx 183 | self.translateY = self.dy 184 | self.scaleX = 1 185 | self.scaleY = 1 186 | self.angle1 = 0 187 | self.angle2 = 0 188 | 189 | if self.eq(self.xy, 0) and self.eq(self.yx, 0): 190 | self.scaleX = self.xx 191 | self.scaleY = self.yy 192 | 193 | elif self.eq(self.xx * self.yx, -self.xy * self.yy): 194 | self._decomposeScaleRotate() 195 | 196 | elif self.eq(self.xx * self.xy, -self.yx * self.yy): 197 | self._decomposeRotateScale() 198 | 199 | else: 200 | transpose = self.transpose() 201 | (vx1, vy1), (vx2, vy2) = self._eigenvalueDecomposition(self.matrix, transpose.matrix) 202 | u = self.__class__(vx1, vx2, vy1, vy2, 0, 0) 203 | 204 | (vx1, vy1), (vx2, vy2) = self._eigenvalueDecomposition(transpose.matrix, self.matrix) 205 | vt = self.__class__(vx1, vy1, vx2, vy2, 0, 0) 206 | 207 | s = self.__class__(self.__class__().reverseTransform(u), self, self.__class__().reverseTransform(vt)) 208 | 209 | vt._decomposeScaleRotate() 210 | self.angle1 = -vt.angle2 211 | 212 | u._decomposeRotateScale() 213 | self.angle2 = -u.angle1 214 | 215 | self.scaleX = s.xx * vt.scaleX * u.scaleX 216 | self.scaleY = s.yy * vt.scaleY * u.scaleY 217 | 218 | return (self.translateX, self.translateY), (self.scaleX, self.scaleY), (self.angle1, self.angle2) 219 | 220 | def _decomposeScaleRotate(self): 221 | sign = self.scaleSign() 222 | a = (math.atan2(self.yx, self.yy) + math.atan2(-sign * self.xy, sign * self.xx)) * .5 223 | c = math.cos(a) 224 | s = math.sin(a) 225 | if c == 0: ## ???? 226 | c = 0.0000000000000000000000000000000001 227 | if s == 0: 228 | s = 0.0000000000000000000000000000000001 229 | self.angle2 = -a 230 | self.scaleX = self.calcFromValues(self.xx / float(c), c, -self.xy / float(s), s) 231 | self.scaleY = self.calcFromValues(self.yy / float(c), c, self.yx / float(s), s) 232 | 233 | def _decomposeRotateScale(self): 234 | sign = self.scaleSign() 235 | a = (math.atan2(sign * self.yx, sign * self.xx) + math.atan2(-self.xy, self.yy)) * .5 236 | c = math.cos(a) 237 | s = math.sin(a) 238 | if c == 0: 239 | c = 0.0000000000000000000000000000000001 240 | if s == 0: 241 | s = 0.0000000000000000000000000000000001 242 | self.angle1 = -a 243 | self.scaleX = self.calcFromValues(self.xx / float(c), c, self.yx / float(s), s) 244 | self.scaleY = self.calcFromValues(self.yy / float(c), c, -self.xy / float(s), s) 245 | 246 | def _eigenvalueDecomposition(self, *matrixes): 247 | m = self.__class__(*matrixes) 248 | b = -m.xx - m.yy 249 | c = m.xx * m.yy - m.xy * m.yx 250 | d = math.sqrt(abs(b * b - 4 * c)) 251 | if b < 0: 252 | d *= -1 253 | l1 = -(b + d) * .5 254 | l2 = c / float(l1) 255 | 256 | vx1 = vy2 = None 257 | if l1 - m.xx != 0: 258 | vx1 = m.xy / (l1 - m.xx) 259 | vy1 = 1 260 | elif m.xy != 0: 261 | vx1 = 1 262 | vy1 = (l1 - m.xx) / m.xy 263 | elif m.yx != 0: 264 | vx1 = (l1 - m.yy) / m.yx 265 | vy1 = 1 266 | elif l1 - m.yy != 0: 267 | vx1 = 1 268 | vy1 = m.yx / (l1 - m.yy) 269 | 270 | vx2 = vy2 = None 271 | if l2 - m.xx != 0: 272 | vx2 = m.xy / (l2 - m.xx) 273 | vy2 = 1 274 | elif m.xy != 0: 275 | vx2 = 1 276 | vy2 = (l2 - m.xx) / m.xy 277 | elif m.yx != 0: 278 | vx2 = (l2 - m.yy) / m.yx 279 | vy2 = 1 280 | elif l2 - m.yy != 0: 281 | vx2 = 1 282 | vy2 = m.yx / (l2 - m.yy) 283 | 284 | 285 | if self.eq(l1, l2): 286 | vx1 = 1 287 | vy1 = 0 288 | vx2 = 0 289 | vy2 = 1 290 | 291 | d1 = math.sqrt(vx1 * vx1 + vy1 * vy1) 292 | d2 = math.sqrt(vx2 * vx2 + vy2 * vy2) 293 | 294 | vx1 /= d1 295 | vy1 /= d1 296 | vx2 /= d2 297 | vy2 /= d2 298 | 299 | return (vx1, vy1), (vx2, vy2) 300 | 301 | def compose(self, translate, scale, angle): 302 | translateX, translateY = translate 303 | scaleX, scaleY = scale 304 | angle1, angle2 = angle 305 | matrix = self.transformClass() 306 | matrix = matrix.translate(translateX, translateY) 307 | matrix = matrix.rotate(angle2) 308 | matrix = matrix.scale(scaleX, scaleY) 309 | matrix = matrix.rotate(angle1) 310 | return self.__class__(matrix) 311 | 312 | def _interpolate(self, v1, v2, value): 313 | return v1 * (1 - value) + v2 * value 314 | 315 | def interpolate(self, other, value): 316 | if isinstance(value, (int, float)): 317 | x = y = value 318 | else: 319 | x, y = value 320 | 321 | self.decompose() 322 | other.decompose() 323 | 324 | translateX = self._interpolate(self.translateX, other.translateX, x) 325 | translateY = self._interpolate(self.translateY, other.translateY, y) 326 | scaleX = self._interpolate(self.scaleX, other.scaleX, x) 327 | scaleY = self._interpolate(self.scaleY, other.scaleY, y) 328 | angle1 = self._interpolate(self.angle1, other.angle1, x) 329 | angle2 = self._interpolate(self.angle2, other.angle2, y) 330 | return self.compose((translateX, translateY), (scaleX, scaleY), (angle1, angle2)) 331 | 332 | 333 | class FontMathWarning(Exception): pass 334 | 335 | def _interpolateValue(data1, data2, value): 336 | return data1 * (1 - value) + data2 * value 337 | 338 | def _linearInterpolationTransformMatrix(matrix1, matrix2, value): 339 | """ Linear, 'oldstyle' interpolation of the transform matrix.""" 340 | return tuple(_interpolateValue(matrix1[i], matrix2[i], value) for i in range(len(matrix1))) 341 | 342 | def _polarDecomposeInterpolationTransformation(matrix1, matrix2, value): 343 | """ Interpolate using the MathTransform method. """ 344 | m1 = MathTransform(matrix1) 345 | m2 = MathTransform(matrix2) 346 | return tuple(m1.interpolate(m2, value)) 347 | 348 | def _mathPolarDecomposeInterpolationTransformation(matrix1, matrix2, value): 349 | """ Interpolation with ShallowTransfor, wrapped by decompose / compose actions.""" 350 | off, scl, rot = MathTransform(matrix1).decompose() 351 | m1 = ShallowTransform(off, scl, rot) 352 | off, scl, rot = MathTransform(matrix2).decompose() 353 | m2 = ShallowTransform(off, scl, rot) 354 | m3 = m1 + value * (m2-m1) 355 | m3 = MathTransform().compose(m3.offset, m3.scale, m3.rotation) 356 | return tuple(m3) 357 | 358 | 359 | if __name__ == "__main__": 360 | from random import random 361 | import sys 362 | import doctest 363 | sys.exit(doctest.testmod().failed) 364 | -------------------------------------------------------------------------------- /Lib/fontMath/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robotools/fontMath/0ad923dfa4b07f2857639a81bd2ae62cae44dce5/Lib/fontMath/test/__init__.py -------------------------------------------------------------------------------- /Lib/fontMath/test/test_mathFunctions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from fontTools.misc.fixedTools import otRound 3 | from fontMath.mathFunctions import ( 4 | add, addPt, sub, subPt, mul, mulPt, div, divPt, factorAngle, _roundNumber, 5 | setRoundIntegerFunction, setRoundFloatFunction, 6 | _ROUND_INTEGER_FUNC, _ROUND_FLOAT_FUNC, round2 7 | ) 8 | 9 | 10 | class MathFunctionsTest(unittest.TestCase): 11 | def __init__(self, methodName): 12 | unittest.TestCase.__init__(self, methodName) 13 | 14 | def test_add(self): 15 | self.assertEqual(add(1, 2), 3) 16 | self.assertEqual(add(3, -4), -1) 17 | self.assertEqual(add(5, 0), 5) 18 | self.assertEqual(add(6, 7), add(7, 6)) 19 | 20 | def test_addPt(self): 21 | self.assertEqual(addPt((20, 230), (50, 40)), (70, 270)) 22 | 23 | def test_sub(self): 24 | self.assertEqual(sub(10, 10), 0) 25 | self.assertEqual(sub(10, -10), 20) 26 | self.assertEqual(sub(-10, 10), -20) 27 | 28 | def test_subPt(self): 29 | self.assertEqual(subPt((20, 230), (50, 40)), (-30, 190)) 30 | 31 | def test_mul(self): 32 | self.assertEqual(mul(1, 2), 2) 33 | self.assertEqual(mul(3, -4), -12) 34 | self.assertEqual(mul(10, 0), 0) 35 | self.assertEqual(mul(5, 6), mul(6, 5)) 36 | 37 | def test_mulPt(self): 38 | self.assertEqual(mulPt((15, 25), (2, 3)), (30, 75)) 39 | 40 | def test_div(self): 41 | self.assertEqual(div(1, 2), 0.5) 42 | with self.assertRaises(ZeroDivisionError): 43 | div(10, 0) 44 | self.assertEqual(div(12, -4), -3) 45 | 46 | def test_divPt(self): 47 | self.assertEqual(divPt((15, 75), (2, 3)), (7.5, 25.0)) 48 | 49 | def test_factorAngle(self): 50 | f = factorAngle(5, (2, 1.5), mul) 51 | self.assertEqual(_roundNumber(f, 2), 3.75) 52 | 53 | def test_roundNumber(self): 54 | # round to integer: 55 | self.assertEqual(_roundNumber(0), 0) 56 | self.assertEqual(_roundNumber(0.1), 0) 57 | self.assertEqual(_roundNumber(0.99), 1) 58 | self.assertEqual(_roundNumber(0.499), 0) 59 | self.assertEqual(_roundNumber(0.5), 0) 60 | self.assertEqual(_roundNumber(-0.499), 0) 61 | self.assertEqual(_roundNumber(-0.5), 0) 62 | self.assertEqual(_roundNumber(1.5), 2) 63 | self.assertEqual(_roundNumber(-1.5), -2) 64 | 65 | def test_roundNumber_to_float(self): 66 | # round to float with specified decimals: 67 | self.assertEqual(_roundNumber(0.3333, None), 0) 68 | self.assertEqual(_roundNumber(0.3333, 0), 0.0) 69 | self.assertEqual(_roundNumber(0.3333, 1), 0.3) 70 | self.assertEqual(_roundNumber(0.3333, 2), 0.33) 71 | self.assertEqual(_roundNumber(0.3333, 3), 0.333) 72 | 73 | def test_set_custom_round_integer_func(self): 74 | default = _ROUND_INTEGER_FUNC 75 | try: 76 | setRoundIntegerFunction(otRound) 77 | self.assertEqual(_roundNumber(0.5), 1) 78 | self.assertEqual(_roundNumber(-1.5), -1) 79 | finally: 80 | setRoundIntegerFunction(default) 81 | 82 | def test_set_custom_round_float_func(self): 83 | default = _ROUND_FLOAT_FUNC 84 | try: 85 | setRoundFloatFunction(round2) 86 | self.assertEqual(_roundNumber(0.55, 1), 0.6) 87 | self.assertEqual(_roundNumber(-1.555, 2), -1.55) 88 | finally: 89 | setRoundIntegerFunction(default) 90 | 91 | 92 | if __name__ == "__main__": 93 | unittest.main() 94 | -------------------------------------------------------------------------------- /Lib/fontMath/test/test_mathGlyph.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import unittest 3 | from fontTools.pens.pointPen import AbstractPointPen 4 | from fontMath.mathFunctions import addPt, mulPt 5 | from fontMath.mathGlyph import ( 6 | MathGlyph, MathGlyphPen, FilterRedundantPointPen, 7 | _processMathOneContours, _processMathTwoContours, _anchorTree, 8 | _pairAnchors, _processMathOneAnchors, _processMathTwoAnchors, 9 | _pairComponents, _processMathOneComponents, _processMathTwoComponents, 10 | _expandImage, _compressImage, _pairImages, _processMathOneImage, 11 | _processMathTwoImage, _processMathOneTransformation, 12 | _processMathTwoTransformation, _roundContours, _roundTransformation, 13 | _roundImage, _roundComponents, _roundAnchors 14 | ) 15 | 16 | 17 | try: 18 | basestring, xrange 19 | range = xrange 20 | except NameError: 21 | basestring = str 22 | 23 | 24 | class MathGlyphTest(unittest.TestCase): 25 | def __init__(self, methodName): 26 | unittest.TestCase.__init__(self, methodName) 27 | 28 | def _setupTestGlyph(self): 29 | glyph = MathGlyph(None) 30 | glyph.width = 0 31 | glyph.height = 0 32 | return glyph 33 | 34 | def test__eq__(self): 35 | glyph1 = self._setupTestGlyph() 36 | glyph2 = self._setupTestGlyph() 37 | self.assertEqual(glyph1, glyph2) 38 | 39 | glyph2.width = 1 40 | self.assertFalse(glyph1 == glyph2) 41 | 42 | nonglyph = object() 43 | self.assertFalse(glyph1 == nonglyph) 44 | 45 | glyph1 = MathGlyph(None) 46 | glyph1.name = 'space' 47 | glyph1.width = 100 48 | 49 | class MyGlyph(object): 50 | pass 51 | other = MyGlyph() 52 | other.name = 'space' 53 | other.width = 100 54 | other.height = None 55 | other.contours = [] 56 | other.components = [] 57 | other.anchors = [] 58 | other.guidelines = [] 59 | other.image = {'fileName': None, 60 | 'transformation': (1, 0, 0, 1, 0, 0), 61 | 'color': None} 62 | other.lib = {} 63 | other.unicodes = None 64 | other.note = None 65 | self.assertEqual(glyph1, other) 66 | 67 | def test__ne__(self): 68 | glyph1 = self._setupTestGlyph() 69 | glyph2 = MathGlyph(None) 70 | glyph2.width = 1 71 | glyph2.name = 'a' 72 | self.assertNotEqual(glyph1, glyph2) 73 | self.assertNotEqual(glyph1, 'foo') 74 | 75 | def test_width_add(self): 76 | glyph1 = self._setupTestGlyph() 77 | glyph1.width = 1 78 | glyph2 = self._setupTestGlyph() 79 | glyph2.width = 2 80 | glyph3 = glyph1 + glyph2 81 | self.assertEqual(glyph3.width, 3) 82 | 83 | def test_width_sub(self): 84 | glyph1 = self._setupTestGlyph() 85 | glyph1.width = 3 86 | glyph2 = self._setupTestGlyph() 87 | glyph2.width = 2 88 | glyph3 = glyph1 - glyph2 89 | self.assertEqual(glyph3.width, 1) 90 | 91 | def test_width_mul(self): 92 | glyph1 = self._setupTestGlyph() 93 | glyph1.width = 2 94 | glyph2 = glyph1 * 3 95 | self.assertEqual(glyph2.width, 6) 96 | glyph1 = self._setupTestGlyph() 97 | glyph1.width = 2 98 | glyph2 = glyph1 * (3, 1) 99 | self.assertEqual(glyph2.width, 6) 100 | 101 | def test_width_div(self): 102 | glyph1 = self._setupTestGlyph() 103 | glyph1.width = 7 104 | glyph2 = glyph1 / 2 105 | self.assertEqual(glyph2.width, 3.5) 106 | glyph1 = self._setupTestGlyph() 107 | glyph1.width = 7 108 | glyph2 = glyph1 / (2, 1) 109 | self.assertEqual(glyph2.width, 3.5) 110 | 111 | def test_width_round(self): 112 | glyph1 = self._setupTestGlyph() 113 | glyph1.width = 6.99 114 | glyph2 = glyph1.round() 115 | self.assertEqual(glyph2.width, 7) 116 | 117 | def test_height_add(self): 118 | glyph1 = self._setupTestGlyph() 119 | glyph1.height = 1 120 | glyph2 = self._setupTestGlyph() 121 | glyph2.height = 2 122 | glyph3 = glyph1 + glyph2 123 | self.assertEqual(glyph3.height, 3) 124 | 125 | def test_height_sub(self): 126 | glyph1 = self._setupTestGlyph() 127 | glyph1.height = 3 128 | glyph2 = self._setupTestGlyph() 129 | glyph2.height = 2 130 | glyph3 = glyph1 - glyph2 131 | self.assertEqual(glyph3.height, 1) 132 | 133 | def test_height_mul(self): 134 | glyph1 = self._setupTestGlyph() 135 | glyph1.height = 2 136 | glyph2 = glyph1 * 3 137 | self.assertEqual(glyph2.height, 6) 138 | glyph1 = self._setupTestGlyph() 139 | glyph1.height = 2 140 | glyph2 = glyph1 * (1, 3) 141 | self.assertEqual(glyph2.height, 6) 142 | 143 | def test_height_div(self): 144 | glyph1 = self._setupTestGlyph() 145 | glyph1.height = 7 146 | glyph2 = glyph1 / 2 147 | self.assertEqual(glyph2.height, 3.5) 148 | glyph1 = self._setupTestGlyph() 149 | glyph1.height = 7 150 | glyph2 = glyph1 / (1, 2) 151 | self.assertEqual(glyph2.height, 3.5) 152 | 153 | def test_height_round(self): 154 | glyph1 = self._setupTestGlyph() 155 | glyph1.height = 6.99 156 | glyph2 = glyph1.round() 157 | self.assertEqual(glyph2.height, 7) 158 | 159 | def test_contours_add(self): 160 | glyph1 = self._setupTestGlyph() 161 | glyph1.contours = [ 162 | dict(identifier="contour 1", 163 | points=[("line", (0.55, 3.1), False, "test", "1")]), 164 | dict(identifier="contour 1", 165 | points=[("line", (0.55, 3.1), True, "test", "1")]) 166 | ] 167 | glyph2 = self._setupTestGlyph() 168 | glyph2.contours = [ 169 | dict(identifier="contour 1", 170 | points=[("line", (1.55, 4.1), False, "test", "1")]), 171 | dict(identifier="contour 1", 172 | points=[("line", (1.55, 4.1), True, "test", "1")]) 173 | ] 174 | glyph3 = glyph1 + glyph2 175 | expected = [ 176 | dict(identifier="contour 1", 177 | points=[("line", (0.55 + 1.55, 3.1 + 4.1), False, "test", 178 | "1")]), 179 | dict(identifier="contour 1", 180 | points=[("line", (0.55 + 1.55, 3.1 + 4.1), True, "test", 181 | "1")]) 182 | ] 183 | self.assertEqual(glyph3.contours, expected) 184 | 185 | def test_contours_sub(self): 186 | glyph1 = self._setupTestGlyph() 187 | glyph1.contours = [ 188 | dict(identifier="contour 1", 189 | points=[("line", (0.55, 3.1), False, "test", "1")]), 190 | dict(identifier="contour 1", 191 | points=[("line", (0.55, 3.1), True, "test", "1")]) 192 | ] 193 | glyph2 = self._setupTestGlyph() 194 | glyph2.contours = [ 195 | dict(identifier="contour 1", 196 | points=[("line", (1.55, 4.1), False, "test", "1")]), 197 | dict(identifier="contour 1", 198 | points=[("line", (1.55, 4.1), True, "test", "1")]) 199 | ] 200 | glyph3 = glyph1 - glyph2 201 | expected = [ 202 | dict(identifier="contour 1", 203 | points=[("line", (0.55 - 1.55, 3.1 - 4.1), False, "test", 204 | "1")]), 205 | dict(identifier="contour 1", 206 | points=[("line", (0.55 - 1.55, 3.1 - 4.1), True, "test", 207 | "1")]) 208 | ] 209 | self.assertEqual(glyph3.contours, expected) 210 | 211 | def test_contours_mul(self): 212 | glyph1 = self._setupTestGlyph() 213 | glyph1.contours = [ 214 | dict(identifier="contour 1", 215 | points=[("line", (0.55, 3.1), False, "test", "1")]), 216 | dict(identifier="contour 1", 217 | points=[("line", (0.55, 3.1), True, "test", "1")]) 218 | ] 219 | glyph2 = glyph1 * 2 220 | expected = [ 221 | dict(identifier="contour 1", 222 | points=[("line", (0.55 * 2, 3.1 * 2), False, "test", 223 | "1")]), 224 | dict(identifier="contour 1", 225 | points=[("line", (0.55 * 2, 3.1 * 2), True, "test", 226 | "1")]) 227 | ] 228 | self.assertEqual(glyph2.contours, expected) 229 | 230 | def test_contours_div(self): 231 | glyph1 = self._setupTestGlyph() 232 | glyph1.contours = [ 233 | dict(identifier="contour 1", 234 | points=[("line", (1, 3.4), False, "test", "1")]), 235 | dict(identifier="contour 1", 236 | points=[("line", (2, 3.1), True, "test", "1")]) 237 | ] 238 | glyph2 = glyph1 / 2 239 | expected = [ 240 | dict(identifier="contour 1", 241 | points=[("line", (1/2, 3.4/2), False, "test", 242 | "1")]), 243 | dict(identifier="contour 1", 244 | points=[("line", (2/2, 3.1/2), True, "test", 245 | "1")]) 246 | ] 247 | self.assertEqual(glyph2.contours, expected) 248 | 249 | def test_contours_round(self): 250 | glyph1 = self._setupTestGlyph() 251 | glyph1.contours = [ 252 | dict(identifier="contour 1", 253 | points=[("line", (0.55, 3.1), False, "test", "1")]), 254 | dict(identifier="contour 1", 255 | points=[("line", (0.55, 3.1), True, "test", "1")]) 256 | ] 257 | glyph2 = glyph1.round() 258 | expected = [ 259 | dict(identifier="contour 1", 260 | points=[("line", (1, 3), False, "test", "1")]), 261 | dict(identifier="contour 1", 262 | points=[("line", (1, 3), True, "test", "1")]) 263 | ] 264 | self.assertEqual(glyph2.contours, expected) 265 | 266 | def test_components_round(self): 267 | glyph1 = self._setupTestGlyph() 268 | glyph1.components = [ 269 | dict(baseGlyph="A", transformation=(1, 2, 3, 4, 5.1, 5.99), 270 | identifier="1"), 271 | ] 272 | glyph2 = glyph1.round() 273 | expected = [ 274 | dict(baseGlyph="A", transformation=(1, 2, 3, 4, 5, 6), 275 | identifier="1") 276 | ] 277 | self.assertEqual(glyph2.components, expected) 278 | 279 | def test_guidelines_add_same_name_identifier_x_y_angle(self): 280 | glyph1 = self._setupTestGlyph() 281 | glyph1.guidelines = [ 282 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 283 | dict(name="foo", identifier="2", x=3, y=4, angle=2) 284 | ] 285 | glyph2 = self._setupTestGlyph() 286 | glyph2.guidelines = [ 287 | dict(name="foo", identifier="2", x=3, y=4, angle=2), 288 | dict(name="foo", identifier="1", x=1, y=2, angle=1) 289 | ] 290 | expected = [ 291 | dict(name="foo", identifier="1", x=2, y=4, angle=2), 292 | dict(name="foo", identifier="2", x=6, y=8, angle=4) 293 | ] 294 | glyph3 = glyph1 + glyph2 295 | self.assertEqual(glyph3.guidelines, expected) 296 | 297 | def test_guidelines_add_same_name_identifier(self): 298 | glyph1 = self._setupTestGlyph() 299 | glyph1.guidelines = [ 300 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 301 | dict(name="foo", identifier="2", x=1, y=2, angle=2), 302 | ] 303 | glyph2 = self._setupTestGlyph() 304 | glyph2.guidelines = [ 305 | dict(name="foo", identifier="2", x=3, y=4, angle=3), 306 | dict(name="foo", identifier="1", x=3, y=4, angle=4) 307 | ] 308 | expected = [ 309 | dict(name="foo", identifier="1", x=4, y=6, angle=5), 310 | dict(name="foo", identifier="2", x=4, y=6, angle=5) 311 | ] 312 | glyph3 = glyph1 + glyph2 313 | self.assertEqual(glyph3.guidelines, expected) 314 | 315 | def test_guidelines_add_same_name_x_y_angle(self): 316 | glyph1 = self._setupTestGlyph() 317 | glyph1.guidelines = [ 318 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 319 | dict(name="foo", identifier="2", x=3, y=4, angle=2), 320 | ] 321 | glyph2 = self._setupTestGlyph() 322 | glyph2.guidelines = [ 323 | dict(name="foo", identifier="3", x=3, y=4, angle=2), 324 | dict(name="foo", identifier="4", x=1, y=2, angle=1) 325 | ] 326 | expected = [ 327 | dict(name="foo", identifier="1", x=2, y=4, angle=2), 328 | dict(name="foo", identifier="2", x=6, y=8, angle=4) 329 | ] 330 | glyph3 = glyph1 + glyph2 331 | self.assertEqual(glyph3.guidelines, expected) 332 | 333 | def test_guidelines_add_same_identifier_x_y_angle(self): 334 | glyph1 = self._setupTestGlyph() 335 | glyph1.guidelines = [ 336 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 337 | dict(name="bar", identifier="2", x=3, y=4, angle=2), 338 | ] 339 | glyph2 = self._setupTestGlyph() 340 | glyph2.guidelines = [ 341 | dict(name="xxx", identifier="2", x=3, y=4, angle=2), 342 | dict(name="yyy", identifier="1", x=1, y=2, angle=1) 343 | ] 344 | expected = [ 345 | dict(name="foo", identifier="1", x=2, y=4, angle=2), 346 | dict(name="bar", identifier="2", x=6, y=8, angle=4) 347 | ] 348 | glyph3 = glyph1 + glyph2 349 | self.assertEqual(glyph3.guidelines, expected) 350 | 351 | def test_guidelines_add_same_name(self): 352 | glyph1 = self._setupTestGlyph() 353 | glyph1.guidelines = [ 354 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 355 | dict(name="bar", identifier="2", x=1, y=2, angle=2), 356 | ] 357 | glyph2 = self._setupTestGlyph() 358 | glyph2.guidelines = [ 359 | dict(name="bar", identifier="3", x=3, y=4, angle=3), 360 | dict(name="foo", identifier="4", x=3, y=4, angle=4) 361 | ] 362 | expected = [ 363 | dict(name="foo", identifier="1", x=4, y=6, angle=5), 364 | dict(name="bar", identifier="2", x=4, y=6, angle=5) 365 | ] 366 | glyph3 = glyph1 + glyph2 367 | self.assertEqual(glyph3.guidelines, expected) 368 | 369 | def test_guidelines_add_same_identifier(self): 370 | glyph1 = self._setupTestGlyph() 371 | glyph1.guidelines = [ 372 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 373 | dict(name="bar", identifier="2", x=1, y=2, angle=2), 374 | ] 375 | glyph2 = self._setupTestGlyph() 376 | glyph2.guidelines = [ 377 | dict(name="xxx", identifier="2", x=3, y=4, angle=3), 378 | dict(name="yyy", identifier="1", x=3, y=4, angle=4) 379 | ] 380 | expected = [ 381 | dict(name="foo", identifier="1", x=4, y=6, angle=5), 382 | dict(name="bar", identifier="2", x=4, y=6, angle=5) 383 | ] 384 | glyph3 = glyph1 + glyph2 385 | self.assertEqual(glyph3.guidelines, expected) 386 | 387 | def test_guidelines_mul(self): 388 | glyph1 = self._setupTestGlyph() 389 | glyph1.guidelines = [ 390 | dict(x=1, y=3, angle=5, name="test", identifier="1", 391 | color="0,0,0,0") 392 | ] 393 | glyph2 = glyph1 * 3 394 | expected = [ 395 | dict(x=1 * 3, y=3 * 3, angle=15, name="test", identifier="1", 396 | color="0,0,0,0") 397 | ] 398 | self.assertEqual(glyph2.guidelines, expected) 399 | 400 | def test_guidelines_round(self): 401 | glyph1 = self._setupTestGlyph() 402 | glyph1.guidelines = [ 403 | dict(x=1.99, y=3.01, angle=5, name="test", identifier="1", 404 | color="0,0,0,0") 405 | ] 406 | glyph2 = glyph1.round() 407 | expected = [ 408 | dict(x=2, y=3, angle=5, name="test", identifier="1", 409 | color="0,0,0,0") 410 | ] 411 | self.assertEqual(glyph2.guidelines, expected) 412 | 413 | def test_guidelines_valid_angle(self): 414 | glyph1 = self._setupTestGlyph() 415 | glyph1.guidelines = [ 416 | dict(name="foo", identifier="1", x=0, y=0, angle=1) 417 | ] 418 | glyph2 = self._setupTestGlyph() 419 | glyph2.guidelines = [ 420 | dict(name="foo", identifier="1", x=0, y=0, angle=359) 421 | ] 422 | 423 | expected_add = [dict(name="foo", identifier="1", x=0, y=0, angle=0)] 424 | glyph3 = glyph1 + glyph2 425 | self.assertEqual(glyph3.guidelines, expected_add) 426 | 427 | expected_sub = [dict(name="foo", identifier="1", x=0, y=0, angle=2)] 428 | glyph4 = glyph1 - glyph2 429 | self.assertEqual(glyph4.guidelines, expected_sub) 430 | 431 | expected_mul = [dict(name="foo", identifier="1", x=0, y=0, angle=355)] 432 | glyph5 = glyph2 * 5 433 | self.assertEqual(glyph5.guidelines, expected_mul) 434 | 435 | expected_div = [dict(name="foo", identifier="1", x=0, y=0, angle=71.8)] 436 | glyph6 = glyph2 / 5 437 | self.assertEqual(glyph6.guidelines, expected_div) 438 | 439 | def test_anchors_add(self): 440 | glyph1 = self._setupTestGlyph() 441 | glyph1.anchors = [ 442 | dict(x=1, y=-2, name="foo", identifier="1", color="0,0,0,0") 443 | ] 444 | glyph2 = self._setupTestGlyph() 445 | glyph2.anchors = [ 446 | dict(x=3, y=-4, name="foo", identifier="1", color="0,0,0,0") 447 | ] 448 | glyph3 = glyph1 + glyph2 449 | expected = [ 450 | dict(x=4, y=-6, name="foo", identifier="1", color="0,0,0,0") 451 | ] 452 | self.assertEqual(glyph3.anchors, expected) 453 | 454 | def test_anchors_sub(self): 455 | glyph1 = self._setupTestGlyph() 456 | glyph1.anchors = [ 457 | dict(x=1, y=-2, name="foo", identifier="1", color="0,0,0,0") 458 | ] 459 | glyph2 = self._setupTestGlyph() 460 | glyph2.anchors = [ 461 | dict(x=3, y=-4, name="foo", identifier="1", color="0,0,0,0") 462 | ] 463 | glyph3 = glyph1 - glyph2 464 | expected = [ 465 | dict(x=-2, y=2, name="foo", identifier="1", color="0,0,0,0") 466 | ] 467 | self.assertEqual(glyph3.anchors, expected) 468 | 469 | def test_anchors_mul(self): 470 | glyph1 = self._setupTestGlyph() 471 | glyph1.anchors = [ 472 | dict(x=1, y=-2, name="foo", identifier="1", color="0,0,0,0") 473 | ] 474 | glyph2 = glyph1 * 2 475 | expected = [ 476 | dict(x=2, y=-4, name="foo", identifier="1", color="0,0,0,0") 477 | ] 478 | self.assertEqual(glyph2.anchors, expected) 479 | 480 | def test_anchors_div(self): 481 | glyph1 = self._setupTestGlyph() 482 | glyph1.anchors = [ 483 | dict(x=1, y=-2, name="foo", identifier="1", color="0,0,0,0") 484 | ] 485 | glyph2 = glyph1 / 2 486 | expected = [ 487 | dict(x=0.5, y=-1, name="foo", identifier="1", color="0,0,0,0") 488 | ] 489 | self.assertEqual(glyph2.anchors, expected) 490 | 491 | def test_anchors_round(self): 492 | glyph1 = self._setupTestGlyph() 493 | glyph1.anchors = [ 494 | dict(x=99.9, y=-100.1, name="foo", identifier="1", color="0,0,0,0") 495 | ] 496 | glyph2 = glyph1.round() 497 | expected = [ 498 | dict(x=100, y=-100, name="foo", identifier="1", color="0,0,0,0") 499 | ] 500 | self.assertEqual(glyph2.anchors, expected) 501 | 502 | def test_image_round(self): 503 | glyph1 = self._setupTestGlyph() 504 | glyph1.image = dict(fileName="foo", 505 | transformation=(1, 2, 3, 4, 4.99, 6.01), 506 | color="0,0,0,0") 507 | expected = dict(fileName="foo", transformation=(1, 2, 3, 4, 5, 6), 508 | color="0,0,0,0") 509 | glyph2 = glyph1.round() 510 | self.assertEqual(glyph2.image, expected) 511 | 512 | def test_copy(self): 513 | glyph1 = self._setupTestGlyph() 514 | glyph1.unicodes = [] 515 | glyph2 = glyph1.copy() 516 | self.assertEqual(glyph1, glyph2) 517 | 518 | 519 | class MathGlyphPenTest(unittest.TestCase): 520 | def __init__(self, methodName): 521 | unittest.TestCase.__init__(self, methodName) 522 | 523 | def test_pen_with_lines(self): 524 | pen = MathGlyphPen() 525 | pen.beginPath(identifier="contour 1") 526 | pen.addPoint((0, 100), "line", smooth=False, name="name 1", 527 | identifier="point 1") 528 | pen.addPoint((100, 100), "line", smooth=False, name="name 2", 529 | identifier="point 2") 530 | pen.addPoint((100, 0), "line", smooth=False, name="name 3", 531 | identifier="point 3") 532 | pen.addPoint((0, 0), "line", smooth=False, name="name 4", 533 | identifier="point 4") 534 | pen.endPath() 535 | expected = [ 536 | ("curve", (0, 100), False, "name 1", "point 1"), 537 | (None, (0, 100), False, None, None), 538 | (None, (100, 100), False, None, None), 539 | ("curve", (100, 100), False, "name 2", "point 2"), 540 | (None, (100, 100), False, None, None), 541 | (None, (100, 0), False, None, None), 542 | ("curve", (100, 0), False, "name 3", "point 3"), 543 | (None, (100, 0), False, None, None), 544 | (None, (0, 0), False, None, None), 545 | ("curve", (0, 0), False, "name 4", "point 4"), 546 | (None, (0, 0), False, None, None), 547 | (None, (0, 100), False, None, None), 548 | ] 549 | self.assertEqual(pen.contours[-1]["points"], expected) 550 | self.assertEqual(pen.contours[-1]["identifier"], 'contour 1') 551 | 552 | def test_pen_with_lines_strict(self): 553 | pen = MathGlyphPen(strict=True) 554 | pen.beginPath(identifier="contour 1") 555 | pen.addPoint((0, 100), "line", smooth=False, name="name 1", 556 | identifier="point 1") 557 | pen.addPoint((100, 100), "line", smooth=False, name="name 2", 558 | identifier="point 2") 559 | pen.addPoint((100, 0), "line", smooth=False, name="name 3", 560 | identifier="point 3") 561 | pen.addPoint((0, 0), "line", smooth=False, name="name 4", 562 | identifier="point 4") 563 | pen.endPath() 564 | expected = [ 565 | ("line", (0, 100), False, "name 1", "point 1"), 566 | ("line", (100, 100), False, "name 2", "point 2"), 567 | ("line", (100, 0), False, "name 3", "point 3"), 568 | ("line", (0, 0), False, "name 4", "point 4"), 569 | ] 570 | self.assertEqual(pen.contours[-1]["points"], expected) 571 | self.assertEqual(pen.contours[-1]["identifier"], 'contour 1') 572 | 573 | def test_pen_with_lines_and_curves(self): 574 | pen = MathGlyphPen() 575 | pen.beginPath(identifier="contour 1") 576 | pen.addPoint((0, 50), "curve", smooth=False, name="name 1", 577 | identifier="point 1") 578 | pen.addPoint((50, 100), "line", smooth=False, name="name 2", 579 | identifier="point 2") 580 | pen.addPoint((75, 100), None) 581 | pen.addPoint((100, 75), None) 582 | pen.addPoint((100, 50), "curve", smooth=True, name="name 3", 583 | identifier="point 3") 584 | pen.addPoint((100, 25), None) 585 | pen.addPoint((75, 0), None) 586 | pen.addPoint((50, 0), "curve", smooth=False, name="name 4", 587 | identifier="point 4") 588 | pen.addPoint((25, 0), None) 589 | pen.addPoint((0, 25), None) 590 | pen.endPath() 591 | expected = [ 592 | ("curve", (0, 50), False, "name 1", "point 1"), 593 | (None, (0, 50), False, None, None), 594 | (None, (50, 100), False, None, None), 595 | ("curve", (50, 100), False, "name 2", "point 2"), 596 | (None, (75, 100), False, None, None), 597 | (None, (100, 75), False, None, None), 598 | ("curve", (100, 50), True, "name 3", "point 3"), 599 | (None, (100, 25), False, None, None), 600 | (None, (75, 0), False, None, None), 601 | ("curve", (50, 0), False, "name 4", "point 4"), 602 | (None, (25, 0), False, None, None), 603 | (None, (0, 25), False, None, None), 604 | ] 605 | self.assertEqual(pen.contours[-1]["points"], expected) 606 | self.assertEqual(pen.contours[-1]["identifier"], 'contour 1') 607 | 608 | def test_pen_with_lines_and_curves_strict(self): 609 | pen = MathGlyphPen(strict=True) 610 | pen.beginPath(identifier="contour 1") 611 | pen.addPoint((0, 50), "curve", smooth=False, name="name 1", 612 | identifier="point 1") 613 | pen.addPoint((50, 100), "line", smooth=False, name="name 2", 614 | identifier="point 2") 615 | pen.addPoint((75, 100), None) 616 | pen.addPoint((100, 75), None) 617 | pen.addPoint((100, 50), "curve", smooth=True, name="name 3", 618 | identifier="point 3") 619 | pen.addPoint((100, 25), None) 620 | pen.addPoint((75, 0), None) 621 | pen.addPoint((50, 0), "curve", smooth=False, name="name 4", 622 | identifier="point 4") 623 | pen.addPoint((25, 0), None) 624 | pen.addPoint((0, 25), None) 625 | pen.endPath() 626 | expected = [ 627 | ("curve", (0, 50), False, "name 1", "point 1"), 628 | ("line", (50, 100), False, "name 2", "point 2"), 629 | (None, (75, 100), False, None, None), 630 | (None, (100, 75), False, None, None), 631 | ("curve", (100, 50), True, "name 3", "point 3"), 632 | (None, (100, 25), False, None, None), 633 | (None, (75, 0), False, None, None), 634 | ("curve", (50, 0), False, "name 4", "point 4"), 635 | (None, (25, 0), False, None, None), 636 | (None, (0, 25), False, None, None), 637 | ] 638 | self.assertEqual(pen.contours[-1]["points"], expected) 639 | self.assertEqual(pen.contours[-1]["identifier"], 'contour 1') 640 | 641 | 642 | class _TestPointPen(AbstractPointPen): 643 | 644 | def __init__(self): 645 | self._text = [] 646 | 647 | def dump(self): 648 | return "\n".join(self._text) 649 | 650 | def _prep(self, i): 651 | if isinstance(i, basestring): 652 | i = "\"%s\"" % i 653 | return str(i) 654 | 655 | def beginPath(self, identifier=None, **kwargs): 656 | self._text.append("beginPath(identifier=%s)" % self._prep(identifier)) 657 | 658 | def addPoint(self, pt, segmentType=None, smooth=False, name=None, 659 | identifier=None, **kwargs): 660 | self._text.append( 661 | "addPoint(%s, segmentType=%s, smooth=%s, name=%s, " 662 | "identifier=%s)" % ( 663 | self._prep(pt), 664 | self._prep(segmentType), 665 | self._prep(smooth), 666 | self._prep(name), 667 | self._prep(identifier) 668 | ) 669 | ) 670 | 671 | def endPath(self): 672 | self._text.append("endPath()") 673 | 674 | def addComponent(self, baseGlyph, transformation, identifier=None, 675 | **kwargs): 676 | self._text.append( 677 | "addComponent(baseGlyph=%s, transformation=%s, identifier=%s)" % ( 678 | self._prep(baseGlyph), 679 | self._prep(transformation), 680 | self._prep(identifier) 681 | ) 682 | ) 683 | 684 | 685 | class FilterRedundantPointPenTest(unittest.TestCase): 686 | def __init__(self, methodName): 687 | unittest.TestCase.__init__(self, methodName) 688 | 689 | def test_flushContour(self): 690 | points = [ 691 | ("curve", (0, 100), False, "name 1", "point 1"), 692 | (None, (0, 100), False, None, None), 693 | (None, (100, 100), False, None, None), 694 | ("curve", (100, 100), False, "name 2", "point 2"), 695 | (None, (100, 100), False, None, None), 696 | (None, (100, 0), False, None, None), 697 | ("curve", (100, 0), False, "name 3", "point 3"), 698 | (None, (100, 0), False, None, None), 699 | (None, (0, 0), False, None, None), 700 | ("curve", (0, 0), False, "name 4", "point 4"), 701 | (None, (0, 0), False, None, None), 702 | (None, (0, 100), False, None, None), 703 | ] 704 | testPen = _TestPointPen() 705 | filterPen = FilterRedundantPointPen(testPen) 706 | filterPen.beginPath(identifier="contour 1") 707 | for segmentType, pt, smooth, name, identifier in points: 708 | filterPen.addPoint(pt, segmentType=segmentType, smooth=smooth, 709 | name=name, identifier=identifier) 710 | filterPen.endPath() 711 | self.assertEqual( 712 | testPen.dump(), 713 | 'beginPath(identifier="contour 1")\n' 714 | 'addPoint((0, 100), segmentType="line", smooth=False, ' 715 | 'name="name 1", identifier="point 1")\n' 716 | 'addPoint((100, 100), segmentType="line", smooth=False, ' 717 | 'name="name 2", identifier="point 2")\n' 718 | 'addPoint((100, 0), segmentType="line", smooth=False, ' 719 | 'name="name 3", identifier="point 3")\n' 720 | 'addPoint((0, 0), segmentType="line", smooth=False, ' 721 | 'name="name 4", identifier="point 4")\n' 722 | 'endPath()' 723 | ) 724 | 725 | 726 | class PrivateFuncsTest(unittest.TestCase): 727 | def __init__(self, methodName): 728 | unittest.TestCase.__init__(self, methodName) 729 | 730 | def test_processMathOneContours(self): 731 | contours1 = [ 732 | dict(identifier="contour 1", 733 | points=[("line", (1, 3), False, "test", "1")]) 734 | ] 735 | contours2 = [ 736 | dict(identifier=None, points=[(None, (4, 6), True, None, None)]) 737 | ] 738 | self.assertEqual( 739 | _processMathOneContours(contours1, contours2, addPt), 740 | [ 741 | dict(identifier="contour 1", 742 | points=[("line", (5, 9), False, "test", "1")]) 743 | ] 744 | ) 745 | 746 | def test_processMathTwoContours(self): 747 | contours = [ 748 | dict(identifier="contour 1", 749 | points=[("line", (1, 3), False, "test", "1")]) 750 | ] 751 | self.assertEqual( 752 | _processMathTwoContours(contours, (2, 1.5), mulPt), 753 | [ 754 | dict(identifier="contour 1", 755 | points=[("line", (2, 4.5), False, "test", "1")]) 756 | ] 757 | ) 758 | True 759 | 760 | def test_anchorTree(self): 761 | anchors = [ 762 | dict(identifier="1", name="test", x=1, y=2, color=None), 763 | dict(name="test", x=1, y=2, color=None), 764 | dict(name="test", x=3, y=4, color=None), 765 | dict(name="test", x=2, y=3, color=None), 766 | dict(name="c", x=1, y=2, color=None), 767 | dict(name="a", x=0, y=0, color=None), 768 | ] 769 | self.assertEqual( 770 | list(_anchorTree(anchors).items()), 771 | [ 772 | ("test", [ 773 | ("1", 1, 2, None), 774 | (None, 1, 2, None), 775 | (None, 3, 4, None), 776 | (None, 2, 3, None) 777 | ]), 778 | ("c", [ 779 | (None, 1, 2, None) 780 | ]), 781 | ("a", [ 782 | (None, 0, 0, None) 783 | ]) 784 | ] 785 | ) 786 | 787 | def test_pairAnchors_matching_identifiers(self): 788 | anchors1 = { 789 | "test": [ 790 | (None, 1, 2, None), 791 | ("identifier 1", 3, 4, None) 792 | ] 793 | } 794 | anchors2 = { 795 | "test": [ 796 | ("identifier 1", 1, 2, None), 797 | (None, 3, 4, None) 798 | ] 799 | } 800 | self.assertEqual( 801 | _pairAnchors(anchors1, anchors2), 802 | [ 803 | ( 804 | dict(name="test", identifier=None, x=1, y=2, color=None), 805 | dict(name="test", identifier=None, x=3, y=4, color=None) 806 | ), 807 | ( 808 | dict(name="test", identifier="identifier 1", x=3, y=4, 809 | color=None), 810 | dict(name="test", identifier="identifier 1", x=1, y=2, 811 | color=None) 812 | ) 813 | ] 814 | ) 815 | 816 | def test_pairAnchors_mismatched_identifiers(self): 817 | anchors1 = { 818 | "test": [ 819 | ("identifier 1", 3, 4, None) 820 | ] 821 | } 822 | anchors2 = { 823 | "test": [ 824 | ("identifier 2", 1, 2, None), 825 | ] 826 | } 827 | self.assertEqual( 828 | _pairAnchors(anchors1, anchors2), 829 | [ 830 | ( 831 | dict(name="test", identifier="identifier 1", x=3, y=4, 832 | color=None), 833 | dict(name="test", identifier="identifier 2", x=1, y=2, 834 | color=None) 835 | ) 836 | ] 837 | ) 838 | 839 | def test_processMathOneAnchors(self): 840 | anchorPairs = [ 841 | ( 842 | dict(x=100, y=-100, name="foo", identifier="1", 843 | color="0,0,0,0"), 844 | dict(x=200, y=-200, name="bar", identifier="2", 845 | color="1,1,1,1") 846 | ) 847 | ] 848 | self.assertEqual( 849 | _processMathOneAnchors(anchorPairs, addPt), 850 | [ 851 | dict(x=300, y=-300, name="foo", identifier="1", 852 | color="0,0,0,0") 853 | ] 854 | ) 855 | 856 | def test_processMathTwoAnchors(self): 857 | anchors = [ 858 | dict(x=100, y=-100, name="foo", identifier="1", color="0,0,0,0") 859 | ] 860 | self.assertEqual( 861 | _processMathTwoAnchors(anchors, (2, 1.5), mulPt), 862 | [ 863 | dict(x=200, y=-150, name="foo", identifier="1", 864 | color="0,0,0,0") 865 | ] 866 | ) 867 | 868 | def test_pairComponents(self): 869 | components1 = [ 870 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 871 | identifier="1"), 872 | dict(baseGlyph="B", transformation=(0, 0, 0, 0, 0, 0), 873 | identifier="1"), 874 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 875 | identifier=None) 876 | ] 877 | components2 = [ 878 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 879 | identifier=None), 880 | dict(baseGlyph="B", transformation=(0, 0, 0, 0, 0, 0), 881 | identifier="1"), 882 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 883 | identifier="1") 884 | ] 885 | self.assertEqual( 886 | _pairComponents(components1, components2), 887 | [ 888 | ( 889 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 890 | identifier="1"), 891 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 892 | identifier="1") 893 | ), 894 | ( 895 | dict(baseGlyph="B", transformation=(0, 0, 0, 0, 0, 0), 896 | identifier="1"), 897 | dict(baseGlyph="B", transformation=(0, 0, 0, 0, 0, 0), 898 | identifier="1") 899 | ), 900 | ( 901 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 902 | identifier=None), 903 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 904 | identifier=None) 905 | ), 906 | ] 907 | ) 908 | 909 | components1 = [ 910 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 911 | identifier=None), 912 | dict(baseGlyph="B", transformation=(0, 0, 0, 0, 0, 0), 913 | identifier=None) 914 | ] 915 | components2 = [ 916 | dict(baseGlyph="B", transformation=(0, 0, 0, 0, 0, 0), 917 | identifier=None), 918 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 919 | identifier=None) 920 | ] 921 | self.assertEqual( 922 | _pairComponents(components1, components2), 923 | [ 924 | ( 925 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 926 | identifier=None), 927 | dict(baseGlyph="A", transformation=(0, 0, 0, 0, 0, 0), 928 | identifier=None) 929 | ), 930 | ( 931 | dict(baseGlyph="B", transformation=(0, 0, 0, 0, 0, 0), 932 | identifier=None), 933 | dict(baseGlyph="B", transformation=(0, 0, 0, 0, 0, 0), 934 | identifier=None) 935 | ), 936 | ] 937 | ) 938 | 939 | def test_processMathOneComponents(self): 940 | components = [ 941 | ( 942 | dict(baseGlyph="A", transformation=(1, 3, 5, 7, 9, 11), 943 | identifier="1"), 944 | dict(baseGlyph="A", transformation=(12, 14, 16, 18, 20, 22), 945 | identifier=None) 946 | ) 947 | ] 948 | self.assertEqual( 949 | _processMathOneComponents(components, addPt), 950 | [ 951 | dict(baseGlyph="A", transformation=(13, 17, 21, 25, 29, 33), 952 | identifier="1") 953 | ] 954 | ) 955 | 956 | def test_processMathTwoComponents(self): 957 | components = [ 958 | dict(baseGlyph="A", transformation=(1, 2, 3, 4, 5, 6), identifier="1") 959 | ] 960 | scaled_components = [ 961 | dict(baseGlyph="A", transformation=(2, 4, 4.5, 6, 10, 9), identifier="1") 962 | ] 963 | self.assertEqual( 964 | _processMathTwoComponents(components, (2, 1.5), mulPt), 965 | scaled_components 966 | ) 967 | self.assertEqual( 968 | _processMathTwoComponents( 969 | components, (2, 1.5), mulPt, scaleComponentTransform=True 970 | ), 971 | scaled_components 972 | ) 973 | self.assertEqual( 974 | _processMathTwoComponents( 975 | components, (2, 1.5), mulPt, scaleComponentTransform=False 976 | ), 977 | [ 978 | dict( 979 | baseGlyph="A", 980 | transformation=(1, 2, 3, 4, 10, 9), 981 | identifier="1" 982 | ) 983 | ], 984 | ) 985 | 986 | def test_expandImage(self): 987 | self.assertEqual( 988 | _expandImage(None), 989 | dict(fileName=None, transformation=(1, 0, 0, 1, 0, 0), color=None) 990 | ) 991 | self.assertEqual( 992 | _expandImage(dict(fileName="foo")), 993 | dict(fileName="foo", transformation=(1, 0, 0, 1, 0, 0), color=None) 994 | ) 995 | 996 | def test_compressImage(self): 997 | self.assertEqual( 998 | _compressImage( 999 | dict(fileName="foo", 1000 | transformation=(1, 0, 0, 1, 0, 0), color=None)), 1001 | dict(fileName="foo", color=None, xScale=1, 1002 | xyScale=0, yxScale=0, yScale=1, xOffset=0, yOffset=0) 1003 | ) 1004 | 1005 | def test_pairImages(self): 1006 | image1 = dict(fileName="foo", transformation=(1, 0, 0, 1, 0, 0), 1007 | color=None) 1008 | image2 = dict(fileName="foo", transformation=(2, 0, 0, 2, 0, 0), 1009 | color="0,0,0,0") 1010 | self.assertEqual( 1011 | _pairImages(image1, image2), 1012 | (image1, image2) 1013 | ) 1014 | 1015 | image1 = dict(fileName="foo", transformation=(1, 0, 0, 1, 0, 0), 1016 | color=None) 1017 | image2 = dict(fileName="bar", transformation=(1, 0, 0, 1, 0, 0), 1018 | color=None) 1019 | self.assertEqual( 1020 | _pairImages(image1, image2), 1021 | () 1022 | ) 1023 | 1024 | def test_processMathOneImage(self): 1025 | image1 = dict(fileName="foo", transformation=(1, 3, 5, 7, 9, 11), 1026 | color="0,0,0,0") 1027 | image2 = dict(fileName="bar", transformation=(12, 14, 16, 18, 20, 22), 1028 | color=None) 1029 | self.assertEqual( 1030 | _processMathOneImage((image1, image2), addPt), 1031 | dict(fileName="foo", transformation=(13, 17, 21, 25, 29, 33), 1032 | color="0,0,0,0") 1033 | ) 1034 | 1035 | def test_processMathTwoImage(self): 1036 | image = dict(fileName="foo", transformation=(1, 2, 3, 4, 5, 6), 1037 | color="0,0,0,0") 1038 | self.assertEqual( 1039 | _processMathTwoImage(image, (2, 1.5), mulPt), 1040 | dict(fileName="foo", transformation=(2, 4, 4.5, 6, 10, 9), 1041 | color="0,0,0,0") 1042 | ) 1043 | 1044 | def test_processMathOneTransformation(self): 1045 | transformation1 = (1, 3, 5, 7, 9, 11) 1046 | transformation2 = (12, 14, 16, 18, 20, 22) 1047 | self.assertEqual( 1048 | _processMathOneTransformation(transformation1, transformation2, 1049 | addPt), 1050 | (13, 17, 21, 25, 29, 33) 1051 | ) 1052 | 1053 | def test_processMathTwoTransformation(self): 1054 | transformation = (1, 2, 3, 4, 5, 6) 1055 | self.assertEqual( 1056 | _processMathTwoTransformation(transformation, (2, 1.5), mulPt), 1057 | (2, 4, 4.5, 6, 10, 9) 1058 | ) 1059 | self.assertEqual( 1060 | _processMathTwoTransformation(transformation, (2, 1.5), mulPt, doScale=True), 1061 | (2, 4, 4.5, 6, 10, 9) 1062 | ) 1063 | self.assertEqual( 1064 | _processMathTwoTransformation(transformation, (2, 1.5), mulPt, doScale=False), 1065 | (1, 2, 3, 4, 10, 9) 1066 | ) 1067 | 1068 | def test_roundContours(self): 1069 | contour = [ 1070 | dict(identifier="contour 1", points=[("line", (0.55, 3.1), False, 1071 | "test", "1")]), 1072 | dict(identifier="contour 1", points=[("line", (0.55, 3.1), True, 1073 | "test", "1")]) 1074 | ] 1075 | self.assertEqual( 1076 | _roundContours(contour), 1077 | [ 1078 | dict(identifier="contour 1", points=[("line", (1, 3), False, 1079 | "test", "1")]), 1080 | dict(identifier="contour 1", points=[("line", (1, 3), True, 1081 | "test", "1")]) 1082 | ] 1083 | ) 1084 | 1085 | def test_roundTransformation(self): 1086 | transformation = (1, 2, 3, 4, 4.99, 6.01) 1087 | self.assertEqual( 1088 | _roundTransformation(transformation), 1089 | (1, 2, 3, 4, 5, 6) 1090 | ) 1091 | 1092 | def test_roundImage(self): 1093 | image = dict(fileName="foo", transformation=(1, 2, 3, 4, 4.99, 6.01), 1094 | color="0,0,0,0") 1095 | self.assertEqual( 1096 | _roundImage(image), 1097 | dict(fileName="foo", transformation=(1, 2, 3, 4, 5, 6), 1098 | color="0,0,0,0") 1099 | ) 1100 | 1101 | def test_roundComponents(self): 1102 | components = [ 1103 | dict(baseGlyph="A", transformation=(1, 2, 3, 4, 5.1, 5.99), 1104 | identifier="1"), 1105 | ] 1106 | self.assertEqual( 1107 | _roundComponents(components), 1108 | [ 1109 | dict(baseGlyph="A", transformation=(1, 2, 3, 4, 5, 6), 1110 | identifier="1") 1111 | ] 1112 | ) 1113 | 1114 | def test_roundAnchors(self): 1115 | anchors = [ 1116 | dict(x=99.9, y=-100.1, name="foo", identifier="1", 1117 | color="0,0,0,0") 1118 | ] 1119 | self.assertEqual( 1120 | _roundAnchors(anchors), 1121 | [ 1122 | dict(x=100, y=-100, name="foo", identifier="1", 1123 | color="0,0,0,0") 1124 | ] 1125 | ) 1126 | 1127 | if __name__ == "__main__": 1128 | unittest.main() 1129 | -------------------------------------------------------------------------------- /Lib/fontMath/test/test_mathGuideline.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from fontMath.mathFunctions import add, addPt, mul, _roundNumber 3 | from fontMath.mathGuideline import ( 4 | _expandGuideline, _compressGuideline, _pairGuidelines, 5 | _processMathOneGuidelines, _processMathTwoGuidelines, _roundGuidelines 6 | ) 7 | 8 | 9 | class MathGuidelineTest(unittest.TestCase): 10 | def __init__(self, methodName): 11 | unittest.TestCase.__init__(self, methodName) 12 | 13 | def test_expandGuideline(self): 14 | guideline = dict(x=100, y=None, angle=None) 15 | self.assertEqual( 16 | sorted(_expandGuideline(guideline).items()), 17 | [('angle', 90), ('x', 100), ('y', 0)] 18 | ) 19 | 20 | guideline = dict(y=100, x=None, angle=None) 21 | self.assertEqual( 22 | sorted(_expandGuideline(guideline).items()), 23 | [('angle', 0), ('x', 0), ('y', 100)] 24 | ) 25 | 26 | def test_compressGuideline(self): 27 | guideline = dict(x=100, y=0, angle=90) 28 | self.assertEqual( 29 | sorted(_compressGuideline(guideline).items()), 30 | [('angle', None), ('x', 100), ('y', None)] 31 | ) 32 | 33 | guideline = dict(x=100, y=0, angle=270) 34 | self.assertEqual( 35 | sorted(_compressGuideline(guideline).items()), 36 | [('angle', None), ('x', 100), ('y', None)] 37 | ) 38 | 39 | guideline = dict(y=100, x=0, angle=0) 40 | self.assertEqual( 41 | sorted(_compressGuideline(guideline).items()), 42 | [('angle', None), ('x', None), ('y', 100)] 43 | ) 44 | 45 | guideline = dict(y=100, x=0, angle=180) 46 | self.assertEqual( 47 | sorted(_compressGuideline(guideline).items()), 48 | [('angle', None), ('x', None), ('y', 100)] 49 | ) 50 | 51 | def test_pairGuidelines_name_identifier_x_y_angle(self): 52 | guidelines1 = [ 53 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 54 | dict(name="foo", identifier="2", x=3, y=4, angle=2), 55 | ] 56 | guidelines2 = [ 57 | dict(name="foo", identifier="2", x=3, y=4, angle=2), 58 | dict(name="foo", identifier="1", x=1, y=2, angle=1) 59 | ] 60 | expected = [ 61 | ( 62 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 63 | dict(name="foo", identifier="1", x=1, y=2, angle=1) 64 | ), 65 | ( 66 | dict(name="foo", identifier="2", x=3, y=4, angle=2), 67 | dict(name="foo", identifier="2", x=3, y=4, angle=2) 68 | ) 69 | ] 70 | self.assertEqual( 71 | _pairGuidelines(guidelines1, guidelines2), 72 | expected 73 | ) 74 | 75 | def test_pairGuidelines_name_identifier(self): 76 | guidelines1 = [ 77 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 78 | dict(name="foo", identifier="2", x=1, y=2, angle=2), 79 | ] 80 | guidelines2 = [ 81 | dict(name="foo", identifier="2", x=3, y=4, angle=3), 82 | dict(name="foo", identifier="1", x=3, y=4, angle=4) 83 | ] 84 | expected = [ 85 | ( 86 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 87 | dict(name="foo", identifier="1", x=3, y=4, angle=4) 88 | ), 89 | ( 90 | dict(name="foo", identifier="2", x=1, y=2, angle=2), 91 | dict(name="foo", identifier="2", x=3, y=4, angle=3) 92 | ) 93 | ] 94 | self.assertEqual( 95 | _pairGuidelines(guidelines1, guidelines2), 96 | expected 97 | ) 98 | 99 | def test_pairGuidelines_name_x_y_angle(self): 100 | guidelines1 = [ 101 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 102 | dict(name="foo", identifier="2", x=3, y=4, angle=2), 103 | ] 104 | guidelines2 = [ 105 | dict(name="foo", identifier="3", x=3, y=4, angle=2), 106 | dict(name="foo", identifier="4", x=1, y=2, angle=1) 107 | ] 108 | expected = [ 109 | ( 110 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 111 | dict(name="foo", identifier="4", x=1, y=2, angle=1) 112 | ), 113 | ( 114 | dict(name="foo", identifier="2", x=3, y=4, angle=2), 115 | dict(name="foo", identifier="3", x=3, y=4, angle=2) 116 | ) 117 | ] 118 | self.assertEqual( 119 | _pairGuidelines(guidelines1, guidelines2), 120 | expected 121 | ) 122 | 123 | def test_pairGuidelines_identifier_x_y_angle(self): 124 | guidelines1 = [ 125 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 126 | dict(name="bar", identifier="2", x=3, y=4, angle=2), 127 | ] 128 | guidelines2 = [ 129 | dict(name="xxx", identifier="2", x=3, y=4, angle=2), 130 | dict(name="yyy", identifier="1", x=1, y=2, angle=1) 131 | ] 132 | expected = [ 133 | ( 134 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 135 | dict(name="yyy", identifier="1", x=1, y=2, angle=1) 136 | ), 137 | ( 138 | dict(name="bar", identifier="2", x=3, y=4, angle=2), 139 | dict(name="xxx", identifier="2", x=3, y=4, angle=2) 140 | ) 141 | ] 142 | self.assertEqual( 143 | _pairGuidelines(guidelines1, guidelines2), 144 | expected 145 | ) 146 | 147 | def test_pairGuidelines_name(self): 148 | guidelines1 = [ 149 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 150 | dict(name="bar", identifier="2", x=1, y=2, angle=2), 151 | ] 152 | guidelines2 = [ 153 | dict(name="bar", identifier="3", x=3, y=4, angle=3), 154 | dict(name="foo", identifier="4", x=3, y=4, angle=4) 155 | ] 156 | expected = [ 157 | ( 158 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 159 | dict(name="foo", identifier="4", x=3, y=4, angle=4) 160 | ), 161 | ( 162 | dict(name="bar", identifier="2", x=1, y=2, angle=2), 163 | dict(name="bar", identifier="3", x=3, y=4, angle=3) 164 | ) 165 | ] 166 | self.assertEqual( 167 | _pairGuidelines(guidelines1, guidelines2), 168 | expected 169 | ) 170 | 171 | def test_pairGuidelines_identifier(self): 172 | guidelines1 = [ 173 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 174 | dict(name="bar", identifier="2", x=1, y=2, angle=2), 175 | ] 176 | guidelines2 = [ 177 | dict(name="xxx", identifier="2", x=3, y=4, angle=3), 178 | dict(name="yyy", identifier="1", x=3, y=4, angle=4) 179 | ] 180 | expected = [ 181 | ( 182 | dict(name="foo", identifier="1", x=1, y=2, angle=1), 183 | dict(name="yyy", identifier="1", x=3, y=4, angle=4) 184 | ), 185 | ( 186 | dict(name="bar", identifier="2", x=1, y=2, angle=2), 187 | dict(name="xxx", identifier="2", x=3, y=4, angle=3) 188 | ) 189 | ] 190 | self.assertEqual( 191 | _pairGuidelines(guidelines1, guidelines2), 192 | expected 193 | ) 194 | 195 | def test_processMathOneGuidelines(self): 196 | guidelines = [ 197 | ( 198 | dict(x=1, y=3, angle=5, name="test", identifier="1", color="0,0,0,0"), 199 | dict(x=6, y=8, angle=10, name=None, identifier=None, color=None) 200 | ) 201 | ] 202 | expected = [ 203 | dict(x=7, y=11, angle=15, name="test", identifier="1", color="0,0,0,0") 204 | ] 205 | self.assertEqual( 206 | _processMathOneGuidelines(guidelines, addPt, add), 207 | expected 208 | ) 209 | 210 | def test_processMathTwoGuidelines(self): 211 | guidelines = [ 212 | dict(x=2, y=3, angle=5, name="test", identifier="1", color="0,0,0,0") 213 | ] 214 | expected = [ 215 | dict(x=4, y=4.5, angle=3.75, name="test", identifier="1", color="0,0,0,0") 216 | ] 217 | result = _processMathTwoGuidelines(guidelines, (2, 1.5), mul) 218 | result[0]["angle"] = _roundNumber(result[0]["angle"], 2) 219 | self.assertEqual(result, expected) 220 | 221 | def test_roundGuidelines(self): 222 | guidelines = [ 223 | dict(x=1.99, y=3.01, angle=5, name="test", identifier="1", color="0,0,0,0") 224 | ] 225 | expected = [ 226 | dict(x=2, y=3, angle=5, name="test", identifier="1", color="0,0,0,0") 227 | ] 228 | result = _roundGuidelines(guidelines) 229 | self.assertEqual(result, expected) 230 | -------------------------------------------------------------------------------- /Lib/fontMath/test/test_mathInfo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from fontMath.mathFunctions import _roundNumber 3 | from fontMath.mathInfo import MathInfo, _numberListAttrs 4 | 5 | 6 | class MathInfoTest(unittest.TestCase): 7 | 8 | def __init__(self, methodName): 9 | unittest.TestCase.__init__(self, methodName) 10 | self.maxDiff = None 11 | 12 | def test_add(self): 13 | info1 = MathInfo(_TestInfoObject()) 14 | info2 = MathInfo(_TestInfoObject()) 15 | info3 = info1 + info2 16 | written = {} 17 | expected = {} 18 | for attr, value in _testData.items(): 19 | if value is None: 20 | continue 21 | written[attr] = getattr(info3, attr) 22 | if isinstance(value, list): 23 | expectedValue = [v + v for v in value] 24 | else: 25 | expectedValue = value + value 26 | expected[attr] = expectedValue 27 | self.assertEqual(sorted(expected.items()), sorted(written.items())) 28 | 29 | def test_add_data_subset_1st_operand(self): 30 | info1 = MathInfo(_TestInfoObject(_testDataSubset)) 31 | info2 = MathInfo(_TestInfoObject()) 32 | info3 = info1 + info2 33 | written = {} 34 | expected = {} 35 | for attr, value in _testData.items(): 36 | if value is None: 37 | continue 38 | written[attr] = getattr(info3, attr) 39 | if isinstance(value, list): 40 | expectedValue = [v + v for v in value] 41 | else: 42 | expectedValue = value + value 43 | expected[attr] = expectedValue 44 | self.assertEqual(sorted(expected), sorted(written)) 45 | # self.assertEqual(sorted(expected.items()), sorted(written.items())) 46 | 47 | def test_add_data_subset_2nd_operand(self): 48 | info1 = MathInfo(_TestInfoObject()) 49 | info2 = MathInfo(_TestInfoObject(_testDataSubset)) 50 | info3 = info1 + info2 51 | written = {} 52 | expected = {} 53 | for attr, value in _testData.items(): 54 | if value is None: 55 | continue 56 | written[attr] = getattr(info3, attr) 57 | if isinstance(value, list): 58 | expectedValue = [v + v for v in value] 59 | else: 60 | expectedValue = value + value 61 | expected[attr] = expectedValue 62 | self.assertEqual(sorted(expected), sorted(written)) 63 | # self.assertEqual(sorted(expected.items()), sorted(written.items())) 64 | 65 | def test_sub(self): 66 | info1 = MathInfo(_TestInfoObject()) 67 | info2 = MathInfo(_TestInfoObject()) 68 | info3 = info1 - info2 69 | written = {} 70 | expected = {} 71 | for attr, value in _testData.items(): 72 | if value is None: 73 | continue 74 | written[attr] = getattr(info3, attr) 75 | if isinstance(value, list): 76 | expectedValue = [v - v for v in value] 77 | else: 78 | expectedValue = value - value 79 | expected[attr] = expectedValue 80 | self.assertEqual(sorted(expected.items()), sorted(written.items())) 81 | 82 | def test_mul(self): 83 | info1 = MathInfo(_TestInfoObject()) 84 | info2 = info1 * 2.5 85 | written = {} 86 | expected = {} 87 | for attr, value in _testData.items(): 88 | if value is None: 89 | continue 90 | written[attr] = getattr(info2, attr) 91 | if isinstance(value, list): 92 | expectedValue = [v * 2.5 for v in value] 93 | else: 94 | expectedValue = value * 2.5 95 | expected[attr] = expectedValue 96 | self.assertEqual(sorted(expected.items()), sorted(written.items())) 97 | 98 | def test_angle(self): 99 | info1 = MathInfo(_TestInfoObject()) 100 | info1.italicAngle = 10 101 | 102 | info2 = info1 - info1 103 | self.assertEqual(info2.italicAngle, 0) 104 | info2 = info1 + info1 + info1 105 | self.assertEqual(info2.italicAngle, 30) 106 | info2 = 10 * info1 107 | self.assertEqual(info2.italicAngle, 100) 108 | info2 = info1 / 20 109 | self.assertEqual(info2.italicAngle, 0.5) 110 | 111 | info2 = info1 * 2.5 112 | self.assertEqual(info2.italicAngle, 25) 113 | info2 = info1 * (2.5, 2.5) 114 | self.assertEqual(info2.italicAngle, 25) 115 | info2 = info1 * (2.5, 5) 116 | self.assertEqual(round(info2.italicAngle), 19) 117 | 118 | def test_mul_data_subset(self): 119 | info1 = MathInfo(_TestInfoObject(_testDataSubset)) 120 | info2 = info1 * 2.5 121 | written = {} 122 | expected = {} 123 | for attr, value in _testDataSubset.items(): 124 | if value is None: 125 | continue 126 | written[attr] = getattr(info2, attr) 127 | expected = {"descender": -500.0, 128 | "guidelines": [{"y": 250.0, "x": 0.0, "identifier": "2", 129 | "angle": 0.0, "name": "bar"}], 130 | "postscriptBlueValues": [-25.0, 0.0, 1000.0, 1025.0, 1625.0], 131 | "unitsPerEm": 2500.0} 132 | self.assertEqual(sorted(expected.items()), sorted(written.items())) 133 | 134 | def test_div(self): 135 | info1 = MathInfo(_TestInfoObject()) 136 | info2 = info1 / 2 137 | written = {} 138 | expected = {} 139 | for attr, value in _testData.items(): 140 | if value is None: 141 | continue 142 | written[attr] = getattr(info2, attr) 143 | if isinstance(value, list): 144 | expectedValue = [v / 2 for v in value] 145 | else: 146 | expectedValue = value / 2 147 | expected[attr] = expectedValue 148 | self.assertEqual(sorted(expected), sorted(written)) 149 | 150 | def test_compare_same(self): 151 | info1 = MathInfo(_TestInfoObject()) 152 | info2 = MathInfo(_TestInfoObject()) 153 | self.assertFalse(info1 < info2) 154 | self.assertFalse(info1 > info2) 155 | self.assertEqual(info1, info2) 156 | 157 | def test_compare_different(self): 158 | info1 = MathInfo(_TestInfoObject()) 159 | info2 = MathInfo(_TestInfoObject()) 160 | info2.ascender = info2.ascender - 1 161 | self.assertFalse(info1 < info2) 162 | self.assertTrue(info1 > info2) 163 | self.assertNotEqual(info1, info2) 164 | 165 | def test_weight_name(self): 166 | info1 = MathInfo(_TestInfoObject()) 167 | info2 = MathInfo(_TestInfoObject()) 168 | info2.openTypeOS2WeightClass = 0 169 | info3 = info1 + info2 170 | self.assertEqual(info3.openTypeOS2WeightClass, 500) 171 | self.assertEqual(info3.postscriptWeightName, "Medium") 172 | info2.openTypeOS2WeightClass = 49 173 | info3 = info1 + info2 174 | self.assertEqual(info3.openTypeOS2WeightClass, 549) 175 | self.assertEqual(info3.postscriptWeightName, "Medium") 176 | info2.openTypeOS2WeightClass = 50 177 | info3 = info1 + info2 178 | self.assertEqual(info3.openTypeOS2WeightClass, 550) 179 | self.assertEqual(info3.postscriptWeightName, "Semi-bold") 180 | info2.openTypeOS2WeightClass = 50 181 | info3 = info1 - info2 182 | self.assertEqual(info3.openTypeOS2WeightClass, 450) 183 | self.assertEqual(info3.postscriptWeightName, "Medium") 184 | info2.openTypeOS2WeightClass = 51 185 | info3 = info1 - info2 186 | self.assertEqual(info3.openTypeOS2WeightClass, 449) 187 | self.assertEqual(info3.postscriptWeightName, "Normal") 188 | info2.openTypeOS2WeightClass = 500 189 | info3 = info1 - info2 190 | self.assertEqual(info3.openTypeOS2WeightClass, 0) 191 | self.assertEqual(info3.postscriptWeightName, "Thin") 192 | info2.openTypeOS2WeightClass = 1500 193 | info3 = info1 - info2 194 | self.assertEqual(info3.openTypeOS2WeightClass, -1000) 195 | self.assertEqual(info3.postscriptWeightName, "Thin") 196 | info2.openTypeOS2WeightClass = 500 197 | info3 = info1 + info2 198 | self.assertEqual(info3.openTypeOS2WeightClass, 1000) 199 | self.assertEqual(info3.postscriptWeightName, "Black") 200 | 201 | def test_round(self): 202 | m = _TestInfoObject() 203 | m.ascender = 699.99 204 | m.descender = -199.99 205 | m.xHeight = 399.66 206 | m.postscriptSlantAngle = None 207 | m.postscriptStemSnapH = [80.1, 90.2] 208 | m.guidelines = [{'y': 100.99, 'x': None, 'angle': None, 'name': 'bar'}] 209 | m.italicAngle = -9.4 210 | m.postscriptBlueScale = 0.137 211 | info = MathInfo(m) 212 | info = info.round() 213 | self.assertEqual(info.ascender, 700) 214 | self.assertEqual(info.descender, -200) 215 | self.assertEqual(info.xHeight, 400) 216 | self.assertEqual(m.italicAngle, -9.4) 217 | self.assertEqual(m.postscriptBlueScale, 0.137) 218 | self.assertIsNone(info.postscriptSlantAngle) 219 | self.assertEqual(info.postscriptStemSnapH, [80, 90]) 220 | self.assertEqual( 221 | [sorted(gl.items()) for gl in info.guidelines], 222 | [[('angle', 0), ('name', 'bar'), ('x', 0), ('y', 101)]] 223 | ) 224 | written = {} 225 | expected = {} 226 | for attr, value in _testData.items(): 227 | if value is None: 228 | continue 229 | written[attr] = getattr(info, attr) 230 | if isinstance(value, list): 231 | expectedValue = [_roundNumber(v) for v in value] 232 | else: 233 | expectedValue = _roundNumber(value) 234 | expected[attr] = expectedValue 235 | self.assertEqual(sorted(expected), sorted(written)) 236 | 237 | def test_sub_undefined_number_list_sets_None(self): 238 | self.assertIn("postscriptBlueValues", _numberListAttrs) 239 | 240 | info1 = _TestInfoObject() 241 | info1.postscriptBlueValues = None 242 | m1 = MathInfo(info1) 243 | 244 | info2 = _TestInfoObject() 245 | info2.postscriptBlueValues = [1, 2, 3] 246 | m2 = MathInfo(info2) 247 | 248 | m3 = m2 - m1 249 | 250 | self.assertIsNone(m3.postscriptBlueValues) 251 | 252 | m4 = m1 - m2 253 | 254 | self.assertIsNone(m4.postscriptBlueValues) 255 | 256 | def test_number_lists_with_different_lengths(self): 257 | self.assertIn("postscriptBlueValues", _numberListAttrs) 258 | 259 | info1 = _TestInfoObject() 260 | info1.postscriptBlueValues = [1, 2] 261 | m1 = MathInfo(info1) 262 | 263 | info2 = _TestInfoObject() 264 | info2.postscriptBlueValues = [1, 2, 3] 265 | m2 = MathInfo(info2) 266 | 267 | m3 = m2 - m1 268 | 269 | self.assertIsNone(m3.postscriptBlueValues) 270 | 271 | m4 = m1 - m2 272 | 273 | self.assertIsNone(m4.postscriptBlueValues) 274 | 275 | m5 = m1 + m2 276 | 277 | self.assertIsNone(m5.postscriptBlueValues) 278 | 279 | 280 | # ---- 281 | # Test Data 282 | # ---- 283 | 284 | _testData = dict( 285 | # generic 286 | unitsPerEm=1000, 287 | descender=-200, 288 | xHeight=400, 289 | capHeight=650, 290 | ascender=700, 291 | italicAngle=5, 292 | # head 293 | openTypeHeadLowestRecPPEM=5, 294 | # hhea 295 | openTypeHheaAscender=700, 296 | openTypeHheaDescender=-200, 297 | openTypeHheaLineGap=200, 298 | openTypeHheaCaretSlopeRise=1, 299 | openTypeHheaCaretSlopeRun=1, 300 | openTypeHheaCaretOffset=1, 301 | # OS/2 302 | openTypeOS2WidthClass=5, 303 | openTypeOS2WeightClass=500, 304 | openTypeOS2TypoAscender=700, 305 | openTypeOS2TypoDescender=-200, 306 | openTypeOS2TypoLineGap=200, 307 | openTypeOS2WinAscent=700, 308 | openTypeOS2WinDescent=-200, 309 | openTypeOS2SubscriptXSize=300, 310 | openTypeOS2SubscriptYSize=300, 311 | openTypeOS2SubscriptXOffset=0, 312 | openTypeOS2SubscriptYOffset=-200, 313 | openTypeOS2SuperscriptXSize=300, 314 | openTypeOS2SuperscriptYSize=300, 315 | openTypeOS2SuperscriptXOffset=0, 316 | openTypeOS2SuperscriptYOffset=500, 317 | openTypeOS2StrikeoutSize=50, 318 | openTypeOS2StrikeoutPosition=300, 319 | # Vhea 320 | openTypeVheaVertTypoAscender=700, 321 | openTypeVheaVertTypoDescender=-200, 322 | openTypeVheaVertTypoLineGap=200, 323 | openTypeVheaCaretSlopeRise=1, 324 | openTypeVheaCaretSlopeRun=1, 325 | openTypeVheaCaretOffset=1, 326 | # postscript 327 | postscriptSlantAngle=-5, 328 | postscriptUnderlineThickness=100, 329 | postscriptUnderlinePosition=-150, 330 | postscriptBlueValues=[-10, 0, 400, 410, 650, 660, 700, 710], 331 | postscriptOtherBlues=[-210, -200], 332 | postscriptFamilyBlues=[-10, 0, 400, 410, 650, 660, 700, 710], 333 | postscriptFamilyOtherBlues=[-210, -200], 334 | postscriptStemSnapH=[80, 90], 335 | postscriptStemSnapV=[110, 130], 336 | postscriptBlueFuzz=1, 337 | postscriptBlueShift=7, 338 | postscriptBlueScale=0.039625, 339 | postscriptDefaultWidthX=400, 340 | postscriptNominalWidthX=400, 341 | # guidelines 342 | guidelines=None 343 | ) 344 | 345 | _testDataSubset = dict( 346 | # generic 347 | unitsPerEm=1000, 348 | descender=-200, 349 | xHeight=None, 350 | # postscript 351 | postscriptBlueValues=[-10, 0, 400, 410, 650], 352 | # guidelines 353 | guidelines=[ 354 | {'y': 100, 'x': None, 'angle': None, 'name': 'bar', 'identifier': '2'} 355 | ] 356 | ) 357 | 358 | 359 | class _TestInfoObject(object): 360 | 361 | def __init__(self, data=_testData): 362 | for attr, value in data.items(): 363 | setattr(self, attr, value) 364 | 365 | if __name__ == "__main__": 366 | unittest.main() 367 | -------------------------------------------------------------------------------- /Lib/fontMath/test/test_mathKerning.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import unittest 3 | from fontMath.mathFunctions import _roundNumber 4 | from fontMath.mathKerning import MathKerning 5 | 6 | 7 | class TestKerning(object): 8 | """Mockup Kerning class""" 9 | def __init__(self): 10 | self._kerning = {} 11 | 12 | def clear(self): 13 | self._kerning = {} 14 | 15 | def asDict(self, returnIntegers=True): 16 | if not returnIntegers: 17 | return self._kerning 18 | kerning = {k: _roundNumber(v) for (k, v) in self._kerning.items()} 19 | return kerning 20 | 21 | def update(self, kerningDict): 22 | self._kerning = {k: v for (k, v) in kerningDict.items() 23 | if v != 0} 24 | 25 | 26 | class TestFont(object): 27 | """Mockup Font class""" 28 | def __init__(self): 29 | self.kerning = TestKerning() 30 | self.groups = {} 31 | 32 | 33 | class MathKerningTest(unittest.TestCase): 34 | 35 | def test_addTo(self): 36 | kerning = { 37 | ("A", "A"): 1, 38 | ("B", "B"): -1, 39 | } 40 | obj = MathKerning(kerning) 41 | obj.addTo(1) 42 | self.assertEqual(sorted(obj.items()), 43 | [(('A', 'A'), 2), (('B', 'B'), 0)]) 44 | 45 | def test_getitem(self): 46 | kerning = { 47 | ("public.kern1.A", "public.kern2.A"): 1, 48 | ("A1", "public.kern2.A"): 2, 49 | ("public.kern1.A", "A2"): 3, 50 | ("A3", "A3"): 4, 51 | } 52 | groups = { 53 | "public.kern1.A": ["A", "A1", "A2", "A3"], 54 | "public.kern2.A": ["A", "A1", "A2", "A3"], 55 | } 56 | obj = MathKerning(kerning, groups) 57 | self.assertEqual(obj["A", "A"], 1) 58 | self.assertEqual(obj["A1", "A"], 2) 59 | self.assertEqual(obj["A", "A2"], 3) 60 | self.assertEqual(obj["A3", "A3"], 4) 61 | self.assertEqual(obj["X", "X"], 0) 62 | self.assertEqual(obj["A3", "public.kern2.A"], 1) 63 | self.assertEqual(sorted(obj.keys())[1], ("A3", "A3")) 64 | self.assertEqual(sorted(obj.values())[3], 4) 65 | 66 | def test_guessPairType(self): 67 | kerning = { 68 | ("public.kern1.A", "public.kern2.A"): 1, 69 | ("A1", "public.kern2.A"): 2, 70 | ("public.kern1.A", "A2"): 3, 71 | ("A3", "A3"): 4, 72 | ("public.kern1.B", "public.kern2.B"): 5, 73 | ("public.kern1.B", "B"): 6, 74 | ("public.kern1.C", "public.kern2.C"): 7, 75 | ("C", "public.kern2.C"): 8, 76 | } 77 | groups = { 78 | "public.kern1.A": ["A", "A1", "A2", "A3"], 79 | "public.kern2.A": ["A", "A1", "A2", "A3"], 80 | "public.kern1.B": ["B"], 81 | "public.kern2.B": ["B"], 82 | "public.kern1.C": ["C"], 83 | "public.kern2.C": ["C"], 84 | } 85 | obj = MathKerning(kerning, groups) 86 | self.assertEqual(obj.guessPairType(("public.kern1.A", "public.kern2.A")), 87 | ('group', 'group')) 88 | self.assertEqual(obj.guessPairType(("A1", "public.kern2.A")), 89 | ('exception', 'group')) 90 | self.assertEqual(obj.guessPairType(("public.kern1.A", "A2")), 91 | ('group', 'exception')) 92 | self.assertEqual(obj.guessPairType(("A3", "A3")), 93 | ('exception', 'exception')) 94 | self.assertEqual(obj.guessPairType(("A", "A")), 95 | ('group', 'group')) 96 | self.assertEqual(obj.guessPairType(("B", "B")), 97 | ('group', 'exception')) 98 | self.assertEqual(obj.guessPairType(("C", "C")), 99 | ('exception', 'group')) 100 | 101 | def test_copy(self): 102 | kerning1 = { 103 | ("A", "A"): 1, 104 | ("B", "B"): 1, 105 | ("NotIn2", "NotIn2"): 1, 106 | ("public.kern1.NotIn2", "C"): 1, 107 | ("public.kern1.D", "public.kern2.D"): 1, 108 | } 109 | groups1 = { 110 | "public.kern1.NotIn1": ["C"], 111 | "public.kern1.D": ["D", "H"], 112 | "public.kern2.D": ["D", "H"], 113 | } 114 | obj1 = MathKerning(kerning1, groups1) 115 | obj2 = obj1.copy() 116 | self.assertEqual(sorted(obj1.items()), sorted(obj2.items())) 117 | 118 | def test_add(self): 119 | kerning1 = { 120 | ("A", "A"): 1, 121 | ("B", "B"): 1, 122 | ("NotIn2", "NotIn2"): 1, 123 | ("public.kern1.NotIn2", "C"): 1, 124 | ("public.kern1.D", "public.kern2.D"): 1, 125 | } 126 | groups1 = { 127 | "public.kern1.NotIn1": ["C"], 128 | "public.kern1.D": ["D", "H"], 129 | "public.kern2.D": ["D", "H"], 130 | } 131 | kerning2 = { 132 | ("A", "A"): -1, 133 | ("B", "B"): 1, 134 | ("NotIn1", "NotIn1"): 1, 135 | ("public.kern1.NotIn1", "C"): 1, 136 | ("public.kern1.D", "public.kern2.D"): 1, 137 | } 138 | groups2 = { 139 | "public.kern1.NotIn2": ["C"], 140 | "public.kern1.D": ["D", "H"], 141 | "public.kern2.D": ["D", "H"], 142 | } 143 | obj = MathKerning(kerning1, groups1) + MathKerning(kerning2, groups2) 144 | self.assertEqual( 145 | sorted(obj.items()), 146 | [(('B', 'B'), 2), 147 | (('NotIn1', 'NotIn1'), 1), 148 | (('NotIn2', 'NotIn2'), 1), 149 | (('public.kern1.D', 'public.kern2.D'), 2), 150 | (('public.kern1.NotIn1', 'C'), 1), 151 | (('public.kern1.NotIn2', 'C'), 1)]) 152 | self.assertEqual( 153 | obj.groups()["public.kern1.D"], 154 | ['D', 'H']) 155 | self.assertEqual( 156 | obj.groups()["public.kern2.D"], 157 | ['D', 'H']) 158 | 159 | def test_add_same_groups(self): 160 | kerning1 = { 161 | ("A", "A"): 1, 162 | ("B", "B"): 1, 163 | ("NotIn2", "NotIn2"): 1, 164 | ("public.kern1.NotIn2", "C"): 1, 165 | ("public.kern1.D", "public.kern2.D"): 1, 166 | } 167 | groups1 = { 168 | "public.kern1.D": ["D", "H"], 169 | "public.kern2.D": ["D", "H"], 170 | } 171 | kerning2 = { 172 | ("A", "A"): -1, 173 | ("B", "B"): 1, 174 | ("NotIn1", "NotIn1"): 1, 175 | ("public.kern1.NotIn1", "C"): 1, 176 | ("public.kern1.D", "public.kern2.D"): 1, 177 | } 178 | groups2 = { 179 | "public.kern1.D": ["D", "H"], 180 | "public.kern2.D": ["D", "H"], 181 | } 182 | obj = MathKerning(kerning1, groups1) + MathKerning(kerning2, groups2) 183 | self.assertEqual( 184 | sorted(obj.items()), 185 | [(('B', 'B'), 2), 186 | (('NotIn1', 'NotIn1'), 1), 187 | (('NotIn2', 'NotIn2'), 1), 188 | (('public.kern1.D', 'public.kern2.D'), 2), 189 | (('public.kern1.NotIn1', 'C'), 1), 190 | (('public.kern1.NotIn2', 'C'), 1)]) 191 | self.assertEqual( 192 | obj.groups()["public.kern1.D"], 193 | ['D', 'H']) 194 | self.assertEqual( 195 | obj.groups()["public.kern2.D"], 196 | ['D', 'H']) 197 | 198 | def test_sub(self): 199 | kerning1 = { 200 | ("A", "A"): 1, 201 | ("B", "B"): 1, 202 | ("NotIn2", "NotIn2"): 1, 203 | ("public.kern1.NotIn2", "C"): 1, 204 | ("public.kern1.D", "public.kern2.D"): 1, 205 | } 206 | groups1 = { 207 | "public.kern1.NotIn1": ["C"], 208 | "public.kern1.D": ["D", "H"], 209 | "public.kern2.D": ["D", "H"], 210 | } 211 | kerning2 = { 212 | ("A", "A"): -1, 213 | ("B", "B"): 1, 214 | ("NotIn1", "NotIn1"): 1, 215 | ("public.kern1.NotIn1", "C"): 1, 216 | ("public.kern1.D", "public.kern2.D"): 1, 217 | } 218 | groups2 = { 219 | "public.kern1.NotIn2": ["C"], 220 | "public.kern1.D": ["D"], 221 | "public.kern2.D": ["D", "H"], 222 | } 223 | obj = MathKerning(kerning1, groups1) - MathKerning(kerning2, groups2) 224 | self.assertEqual( 225 | sorted(obj.items()), 226 | [(('A', 'A'), 2), 227 | (('NotIn1', 'NotIn1'), -1), 228 | (('NotIn2', 'NotIn2'), 1), 229 | (('public.kern1.NotIn1', 'C'), -1), 230 | (('public.kern1.NotIn2', 'C'), 1)]) 231 | self.assertEqual( 232 | obj.groups()["public.kern1.D"], 233 | ['D', 'H']) 234 | self.assertEqual( 235 | obj.groups()["public.kern2.D"], 236 | ['D', 'H']) 237 | 238 | def test_mul(self): 239 | kerning = { 240 | ("A", "A"): 0, 241 | ("B", "B"): 1, 242 | ("C2", "public.kern2.C"): 0, 243 | ("public.kern1.C", "public.kern2.C"): 2, 244 | } 245 | groups = { 246 | "public.kern1.C": ["C1", "C2"], 247 | "public.kern2.C": ["C1", "C2"], 248 | } 249 | obj = MathKerning(kerning, groups) * 2 250 | self.assertEqual( 251 | sorted(obj.items()), 252 | [(('B', 'B'), 2), 253 | (('C2', 'public.kern2.C'), 0), 254 | (('public.kern1.C', 'public.kern2.C'), 4)]) 255 | 256 | def test_mul_tuple_factor(self): 257 | kerning = { 258 | ("A", "A"): 0, 259 | ("B", "B"): 1, 260 | ("C2", "public.kern2.C"): 0, 261 | ("public.kern1.C", "public.kern2.C"): 2, 262 | } 263 | groups = { 264 | "public.kern1.C": ["C1", "C2"], 265 | "public.kern2.C": ["C1", "C2"], 266 | } 267 | obj = MathKerning(kerning, groups) * (3, 2) 268 | self.assertEqual( 269 | sorted(obj.items()), 270 | [(('B', 'B'), 3), 271 | (('C2', 'public.kern2.C'), 0), 272 | (('public.kern1.C', 'public.kern2.C'), 6)]) 273 | 274 | def test_rmul(self): 275 | kerning = { 276 | ("A", "A"): 0, 277 | ("B", "B"): 1, 278 | ("C2", "public.kern2.C"): 0, 279 | ("public.kern1.C", "public.kern2.C"): 2, 280 | } 281 | groups = { 282 | "public.kern1.C": ["C1", "C2"], 283 | "public.kern2.C": ["C1", "C2"], 284 | } 285 | obj = 2 * MathKerning(kerning, groups) 286 | self.assertEqual( 287 | sorted(obj.items()), 288 | [(('B', 'B'), 2), 289 | (('C2', 'public.kern2.C'), 0), 290 | (('public.kern1.C', 'public.kern2.C'), 4)]) 291 | 292 | def test_rmul_tuple_factor(self): 293 | kerning = { 294 | ("A", "A"): 0, 295 | ("B", "B"): 1, 296 | ("C2", "public.kern2.C"): 0, 297 | ("public.kern1.C", "public.kern2.C"): 2, 298 | } 299 | groups = { 300 | "public.kern1.C": ["C1", "C2"], 301 | "public.kern2.C": ["C1", "C2"], 302 | } 303 | obj = (3, 2) * MathKerning(kerning, groups) 304 | self.assertEqual( 305 | sorted(obj.items()), 306 | [(('B', 'B'), 3), 307 | (('C2', 'public.kern2.C'), 0), 308 | (('public.kern1.C', 'public.kern2.C'), 6)]) 309 | 310 | def test_div(self): 311 | kerning = { 312 | ("A", "A"): 0, 313 | ("B", "B"): 4, 314 | ("C2", "public.kern2.C"): 0, 315 | ("public.kern1.C", "public.kern2.C"): 4, 316 | } 317 | groups = { 318 | "public.kern1.C": ["C1", "C2"], 319 | "public.kern2.C": ["C1", "C2"], 320 | } 321 | obj = MathKerning(kerning, groups) / 2 322 | self.assertEqual( 323 | sorted(obj.items()), 324 | [(('B', 'B'), 2), 325 | (('C2', 'public.kern2.C'), 0), 326 | (('public.kern1.C', 'public.kern2.C'), 2)]) 327 | 328 | def test_compare_same_kerning_only(self): 329 | kerning1 = { 330 | ("A", "A"): 0, 331 | ("B", "B"): 4, 332 | } 333 | kerning2 = { 334 | ("A", "A"): 0, 335 | ("B", "B"): 4, 336 | } 337 | mathKerning1 = MathKerning(kerning1, {}) 338 | mathKerning2 = MathKerning(kerning2, {}) 339 | self.assertFalse(mathKerning1 < mathKerning2) 340 | self.assertFalse(mathKerning1 > mathKerning2) 341 | self.assertEqual(mathKerning1, mathKerning2) 342 | 343 | def test_compare_same_kerning_same_groups(self): 344 | kerning1 = { 345 | ("A", "A"): 0, 346 | ("B", "B"): 4, 347 | ("C2", "public.kern2.C"): 0, 348 | ("public.kern1.C", "public.kern2.C"): 4, 349 | } 350 | kerning2 = { 351 | ("A", "A"): 0, 352 | ("B", "B"): 4, 353 | ("C2", "public.kern2.C"): 0, 354 | ("public.kern1.C", "public.kern2.C"): 4, 355 | } 356 | groups = { 357 | "public.kern1.C": ["C1", "C2"], 358 | "public.kern2.C": ["C1", "C2"], 359 | } 360 | mathKerning1 = MathKerning(kerning1, groups) 361 | mathKerning2 = MathKerning(kerning2, groups) 362 | self.assertFalse(mathKerning1 < mathKerning2) 363 | self.assertFalse(mathKerning1 > mathKerning2) 364 | self.assertEqual(mathKerning1, mathKerning2) 365 | 366 | def test_compare_diff_kerning_diff_groups(self): 367 | kerning1 = { 368 | ("A", "A"): 0, 369 | ("B", "B"): 4, 370 | ("C2", "public.kern2.C"): 0, 371 | ("public.kern1.C", "public.kern2.C"): 4, 372 | } 373 | kerning2 = { 374 | ("A", "A"): 0, 375 | ("C2", "public.kern2.C"): 0, 376 | ("public.kern1.C", "public.kern2.C"): 4, 377 | } 378 | groups1 = { 379 | "public.kern1.C": ["C1", "C2"], 380 | "public.kern2.C": ["C1", "C2", "C3"], 381 | } 382 | groups2 = { 383 | "public.kern1.C": ["C1", "C2"], 384 | "public.kern2.C": ["C1", "C2"], 385 | } 386 | mathKerning1 = MathKerning(kerning1, groups1) 387 | mathKerning2 = MathKerning(kerning2, groups2) 388 | self.assertFalse(mathKerning1 < mathKerning2) 389 | self.assertTrue(mathKerning1 > mathKerning2) 390 | self.assertNotEqual(mathKerning1, mathKerning2) 391 | 392 | def test_compare_diff_kerning_same_groups(self): 393 | kerning1 = { 394 | ("A", "A"): 0, 395 | ("B", "B"): 4, 396 | ("C2", "public.kern2.C"): 0, 397 | ("public.kern1.C", "public.kern2.C"): 4, 398 | } 399 | kerning2 = { 400 | ("A", "A"): 0, 401 | ("C2", "public.kern2.C"): 0, 402 | ("public.kern1.C", "public.kern2.C"): 4, 403 | } 404 | groups = { 405 | "public.kern1.C": ["C1", "C2"], 406 | "public.kern2.C": ["C1", "C2"], 407 | } 408 | mathKerning1 = MathKerning(kerning1, groups) 409 | mathKerning2 = MathKerning(kerning2, groups) 410 | self.assertFalse(mathKerning1 < mathKerning2) 411 | self.assertTrue(mathKerning1 > mathKerning2) 412 | self.assertNotEqual(mathKerning1, mathKerning2) 413 | 414 | def test_compare_same_kerning_diff_groups(self): 415 | kerning1 = { 416 | ("A", "A"): 0, 417 | ("B", "B"): 4, 418 | ("C2", "public.kern2.C"): 0, 419 | ("public.kern1.C", "public.kern2.C"): 4, 420 | } 421 | kerning2 = { 422 | ("A", "A"): 0, 423 | ("B", "B"): 4, 424 | ("C2", "public.kern2.C"): 0, 425 | ("public.kern1.C", "public.kern2.C"): 4, 426 | } 427 | groups1 = { 428 | "public.kern1.C": ["C1", "C2"], 429 | "public.kern2.C": ["C1", "C2"], 430 | } 431 | groups2 = { 432 | "public.kern1.C": ["C1", "C2"], 433 | "public.kern2.C": ["C1", "C2", "C3"], 434 | } 435 | mathKerning1 = MathKerning(kerning1, groups1) 436 | mathKerning2 = MathKerning(kerning2, groups2) 437 | self.assertTrue(mathKerning1 < mathKerning2) 438 | self.assertFalse(mathKerning1 > mathKerning2) 439 | self.assertNotEqual(mathKerning1, mathKerning2) 440 | 441 | def test_div_tuple_factor(self): 442 | kerning = { 443 | ("A", "A"): 0, 444 | ("B", "B"): 4, 445 | ("C2", "public.kern2.C"): 0, 446 | ("public.kern1.C", "public.kern2.C"): 4, 447 | } 448 | groups = { 449 | "public.kern1.C": ["C1", "C2"], 450 | "public.kern2.C": ["C1", "C2"], 451 | } 452 | obj = MathKerning(kerning, groups) / (4, 2) 453 | self.assertEqual( 454 | sorted(obj.items()), 455 | [(('B', 'B'), 1), 456 | (('C2', 'public.kern2.C'), 0), 457 | (('public.kern1.C', 'public.kern2.C'), 1)]) 458 | 459 | def test_round(self): 460 | kerning = { 461 | ("A", "A"): 1.99, 462 | ("B", "B"): 4, 463 | ("C", "C"): 7, 464 | ("D", "D"): 9.01, 465 | } 466 | obj = MathKerning(kerning) 467 | obj.round(5) 468 | self.assertEqual( 469 | sorted(obj.items()), 470 | [(('A', 'A'), 0), (('B', 'B'), 5), 471 | (('C', 'C'), 5), (('D', 'D'), 10)]) 472 | 473 | def test_cleanup(self): 474 | kerning = { 475 | ("A", "A"): 0, 476 | ("B", "B"): 1, 477 | ("C", "public.kern2.C"): 0, 478 | ("public.kern1.C", "public.kern2.C"): 1, 479 | ("D", "D"): 1.0, 480 | ("E", "E"): 1.2, 481 | } 482 | groups = { 483 | "public.kern1.C": ["C", "C1"], 484 | "public.kern2.C": ["C", "C1"] 485 | } 486 | obj = MathKerning(kerning, groups) 487 | obj.cleanup() 488 | self.assertEqual( 489 | sorted(obj.items()), 490 | [(('B', 'B'), 1), 491 | (('C', 'public.kern2.C'), 0), 492 | (('D', 'D'), 1), 493 | (('E', 'E'), 1.2), 494 | (('public.kern1.C', 'public.kern2.C'), 1)]) 495 | 496 | def test_extractKerning(self): 497 | kerning = { 498 | ("A", "A"): 0, 499 | ("B", "B"): 1, 500 | ("C", "public.kern2.C"): 0, 501 | ("public.kern1.C", "public.kern2.C"): 1, 502 | ("D", "D"): 1.0, 503 | ("E", "E"): 1.2, 504 | } 505 | groups = { 506 | "public.kern1.C": ["C", "C1"], 507 | "public.kern2.C": ["C", "C1"] 508 | } 509 | font = TestFont() 510 | self.assertEqual(font.kerning.asDict(), {}) 511 | self.assertEqual(list(font.groups.items()), []) 512 | obj = MathKerning(kerning, groups) 513 | obj.extractKerning(font) 514 | self.assertEqual( 515 | sorted(font.kerning.asDict().items()), 516 | [(('B', 'B'), 1), 517 | (('D', 'D'), 1), (('E', 'E'), 1), 518 | (('public.kern1.C', 'public.kern2.C'), 1)]) 519 | self.assertEqual( 520 | sorted(font.groups.items()), 521 | [('public.kern1.C', ['C', 'C1']), 522 | ('public.kern2.C', ['C', 'C1'])]) 523 | 524 | def test_fallback(self): 525 | groups = { 526 | "public.kern1.A" : ["A", "A.alt"], 527 | "public.kern2.O" : ["O", "O.alt"] 528 | } 529 | kerning1 = { 530 | ("A", "O") : 1000, 531 | ("public.kern1.A", "public.kern2.O") : 100 532 | } 533 | kerning2 = { 534 | ("public.kern1.A", "public.kern2.O") : 200 535 | } 536 | 537 | kerning1 = MathKerning(kerning1, groups) 538 | kerning2 = MathKerning(kerning2, groups) 539 | 540 | kerning3 = kerning1 + kerning2 541 | 542 | self.assertEqual( 543 | kerning3["A", "O"], 544 | 1200) 545 | 546 | 547 | if __name__ == "__main__": 548 | unittest.main() 549 | -------------------------------------------------------------------------------- /Lib/fontMath/test/test_mathTransform.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import math 3 | from random import random 4 | from fontMath.mathFunctions import _roundNumber 5 | from fontMath.mathTransform import ( 6 | Transform, FontMathWarning, matrixToMathTransform, mathTransformToMatrix, 7 | _polarDecomposeInterpolationTransformation, 8 | _mathPolarDecomposeInterpolationTransformation, 9 | _linearInterpolationTransformMatrix 10 | ) 11 | 12 | 13 | class MathTransformToFunctionsTest(unittest.TestCase): 14 | def __init__(self, methodName): 15 | unittest.TestCase.__init__(self, methodName) 16 | 17 | def test_matrixToTransform(self): 18 | pass 19 | 20 | def test_TransformToMatrix(self): 21 | pass 22 | 23 | 24 | class ShallowTransformTest(unittest.TestCase): 25 | def __init__(self, methodName): 26 | unittest.TestCase.__init__(self, methodName) 27 | 28 | 29 | _testData = [ 30 | ( 31 | Transform().rotate(math.radians(0)), 32 | Transform().rotate(math.radians(90)) 33 | ), 34 | 35 | ( 36 | Transform().skew(math.radians(60), math.radians(10)), 37 | Transform().rotate(math.radians(90)) 38 | ), 39 | 40 | ( 41 | Transform().scale(.3, 1.3), 42 | Transform().rotate(math.radians(90)) 43 | ), 44 | 45 | ( 46 | Transform().scale(.3, 1.3).rotate(math.radians(-15)), 47 | Transform().rotate(math.radians(90)).scale(.7, .3) 48 | ), 49 | 50 | ( 51 | Transform().translate(250, 250).rotate(math.radians(-15)) 52 | .translate(-250, -250), 53 | Transform().translate(0, 400).rotate(math.radians(80)) 54 | .translate(-100, 0).rotate(math.radians(80)), 55 | ), 56 | 57 | ( 58 | Transform().skew(math.radians(50)).scale(1.5).rotate(math.radians(60)), 59 | Transform().rotate(math.radians(90)) 60 | ), 61 | ] 62 | 63 | 64 | class MathTransformTest(unittest.TestCase): 65 | def __init__(self, methodName): 66 | unittest.TestCase.__init__(self, methodName) 67 | # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 68 | # and fires deprecation warnings if a program uses the old name. 69 | if not hasattr(self, "assertRaisesRegex"): 70 | self.assertRaisesRegex = self.assertRaisesRegexp 71 | 72 | # Disabling this test as it fails intermittently and it's not clear what it 73 | # does (cf. https://github.com/typesupply/fontMath/issues/35) 74 | # 75 | # def test_functions(self): 76 | # """ 77 | # In this test various complex transformations are interpolated using 78 | # 3 different methods: 79 | # - straight linear interpolation, the way glyphMath does it now. 80 | # - using the MathTransform interpolation method. 81 | # - using the ShallowTransform with an initial decompose and final 82 | # compose. 83 | # """ 84 | # value = random() 85 | # testFunctions = [ 86 | # _polarDecomposeInterpolationTransformation, 87 | # _mathPolarDecomposeInterpolationTransformation, 88 | # _linearInterpolationTransformMatrix, 89 | # ] 90 | # with self.assertRaisesRegex( 91 | # FontMathWarning, 92 | # "Minor differences occured when " 93 | # "comparing the interpolation functions."): 94 | # for i, m in enumerate(_testData): 95 | # m1, m2 = m 96 | # results = [] 97 | # for func in testFunctions: 98 | # r = func(m1, m2, value) 99 | # results.append(r) 100 | # if not results[0] == results[1]: 101 | # raise FontMathWarning( 102 | # "Minor differences occured when " 103 | # "comparing the interpolation functions.") 104 | 105 | def _wrapUnWrap(self, precision=12): 106 | """ 107 | Wrap and unwrap a matrix with random values to establish rounding error 108 | """ 109 | t1 = [] 110 | for i in range(6): 111 | t1.append(random()) 112 | m = matrixToMathTransform(t1) 113 | t2 = mathTransformToMatrix(m) 114 | 115 | if not sum([_roundNumber(t1[i] - t2[i], precision) 116 | for i in range(len(t1))]) == 0: 117 | raise FontMathWarning( 118 | "Matrix round-tripping failed for precision value %s." 119 | % (precision)) 120 | 121 | def test_wrapUnWrap(self): 122 | self._wrapUnWrap() 123 | 124 | def test_wrapUnWrapPrecision(self): 125 | """ 126 | Wrap and unwrap should have no rounding errors at least up to 127 | a precision value of 12. 128 | Rounding errors seem to start occuring at a precision value of 14. 129 | """ 130 | for p in range(5, 13): 131 | for i in range(1000): 132 | self._wrapUnWrap(p) 133 | with self.assertRaisesRegex( 134 | FontMathWarning, 135 | "Matrix round-tripping failed for precision value"): 136 | for p in range(14, 16): 137 | for i in range(1000): 138 | self._wrapUnWrap(p) 139 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2005-2009 Type Supply LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include License.txt 2 | include requirements.txt dev-requirements.txt 3 | include tox.ini 4 | include .coveragerc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/robotools/fontMath/workflows/Tests/badge.svg)](https://github.com/robotools/fontMath/actions?query=workflow%3ATests) 2 | [![codecov](https://codecov.io/gh/robotools/fontMath/branch/master/graph/badge.svg)](https://codecov.io/gh/robotools/fontMath) 3 | [![PyPI version fury.io](https://badge.fury.io/py/fontMath.svg)](https://pypi.org/project/fontMath/) 4 | ![Python versions](https://img.shields.io/badge/python-3.8%2C%203.9%2C%203.10%2C%203.11-blue.svg) 5 | 6 | # fontMath 7 | A collection of objects that implement fast font, glyph, etc. math. 8 | 9 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=2.8 2 | virtualenv>=15.0 3 | tox>=2.3 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] 3 | 4 | [tool.setuptools_scm] 5 | write_to = 'Lib/fontMath/_version.py' 6 | write_to_template = '__version__ = "{version}"' -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fonttools==4.43.0 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [aliases] 5 | dist = build_sphinx sdist bdist_wheel 6 | test = pytest 7 | 8 | [metadata] 9 | license_file = License.txt 10 | 11 | [tool:pytest] 12 | minversion = 2.8 13 | testpaths = 14 | Lib/fontMath 15 | python_files = 16 | test_*.py 17 | python_classes = 18 | *Test 19 | addopts = 20 | -v 21 | -r a 22 | --doctest-modules 23 | --doctest-ignore-import-errors 24 | 25 | [options] 26 | setup_requires = setuptools_scm 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | from setuptools import setup, find_packages 4 | import sys 5 | 6 | needs_pytest = {'pytest', 'test'}.intersection(sys.argv) 7 | pytest_runner = ['pytest_runner'] if needs_pytest else [] 8 | needs_wheel = {'bdist_wheel'}.intersection(sys.argv) 9 | wheel = ['wheel'] if needs_wheel else [] 10 | 11 | # with open('README.rst', 'r') as f: 12 | # long_description = f.read() 13 | 14 | setup( 15 | name="fontMath", 16 | description="A set of objects for performing math operations on font data.", 17 | # long_description=long_description, 18 | author="Tal Leming", 19 | author_email="tal@typesupply.com", 20 | url="https://github.com/robotools/fontMath", 21 | license="MIT", 22 | package_dir={"": "Lib"}, 23 | packages=find_packages("Lib"), 24 | include_package_data=True, 25 | test_suite="fontMath", 26 | use_scm_version={ 27 | "write_to": 'Lib/fontMath/_version.py', 28 | "write_to_template": '__version__ = "{version}"', 29 | }, 30 | setup_requires=pytest_runner + wheel + ['setuptools_scm'], 31 | tests_require=[ 32 | 'pytest>=3.0.3', 33 | ], 34 | install_requires=[ 35 | "fonttools>=3.32.0", 36 | ], 37 | classifiers=[ 38 | 'Development Status :: 4 - Beta', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Programming Language :: Python :: 2', 44 | 'Programming Language :: Python :: 3', 45 | 'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based', 46 | 'Topic :: Software Development :: Libraries :: Python Modules', 47 | ], 48 | python_requires='>=3.7', 49 | zip_safe=True, 50 | ) 51 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3{7,8,9,10}-cov, htmlcov 3 | skip_missing_interpreters = true 4 | 5 | [testenv] 6 | deps = 7 | pytest 8 | -rrequirements.txt 9 | cov: coverage>=4.3 10 | install_command = 11 | {envpython} -m pip install -v {opts} {packages} 12 | commands = 13 | # run the test suite against the package installed inside tox env 14 | !cov: pytest {posargs} 15 | # if `*-cov` factor in the tox env name, also collect test coverage 16 | cov: coverage run --parallel-mode -m pytest {posargs} 17 | 18 | [testenv:htmlcov] 19 | deps = 20 | coverage>=4.3 21 | skip_install = true 22 | commands = 23 | coverage combine 24 | coverage html 25 | 26 | [testenv:codecov] 27 | passenv = * 28 | basepython = {env:TOXPYTHON:python} 29 | deps = 30 | coverage>=4.3 31 | codecov 32 | skip_install = true 33 | ignore_outcome = true 34 | commands = 35 | coverage combine 36 | codecov --env TOXENV 37 | --------------------------------------------------------------------------------