pax_global_header00006660000000000000000000000064135630403440014514gustar00rootroot0000000000000052 comment=d9e70775c4dd4d4ada5239880b8e34f5addb1cec booleanOperations-0.9.0/000077500000000000000000000000001356304034400152055ustar00rootroot00000000000000booleanOperations-0.9.0/.gitignore000066400000000000000000000001621356304034400171740ustar00rootroot00000000000000*.py[cod] dist build local MANIFEST Lib/booleanOperations.egg-info .eggs .DS_Store .tox/ .cache/ _version.py booleanOperations-0.9.0/.travis.yml000066400000000000000000000053011356304034400173150ustar00rootroot00000000000000language: python env: global: - TWINE_USERNAME="anthrotype" - secure: grrVsFD+NUzbNZEv9HXhFh48VKnOWatSh4HRvO/k+DFn8iQvj1mXYXyNM7STOgX3lAxryJHpfcie1UvlE6JLPGvM8TKuq4lqtWlHCaDiNOIiaEp4EGxxhLkSz/Zf7D6501OP1hs/8kFO46QHJteOS0/SXcnIC0Fvq2hDqMb6GUoYKWGyxKwvSfT4AdVPWHEe/p2PNyWAMmJwnx1AwggnsxzDg6zAANCEAiEk7iU+gpNTgXmp+DWo/Av9hglidRjSdq8Fn0x4L+PxMxshJhIbdOB5cFS/Lq1LLBF8yqceht+hAj/pzQnIqE6ALYtXgHUxp/W0BJdJWX16m7TXm4NPWgmgRrRLR/amD+Mp5l1M9UHgwSOdQBU22CkF/hfor0b6ED4Q+Ap6ayy+g6cOp29okGLCMIjXxJU/9rIFjYQ5TFfmD2RvVhUj44e7azGyHNa9goro78mcM9on7a377guODVivVNANPTqm2ZEvLprWmLcz/dlEHFb4ODLvvh4x9hYLPZBF1A9j+qhAGuAFtmOPsPBAJzaSEDAu3DWtwYeGh1+RqagoNhLovB/qfz4YbFygWTj7oQfvmnE0wkAFGqR5EbgLl6o4FKvXl5ZpDj8q/gZdCjwi4VLSTguk0xlIfObLhKq73issjB3Jaxi4Pnl5KcnnhhgtvY3WGx27jMP2yB0= matrix: include: - python: 3.6 - python: 3.7 # required to run python3.7 on Travis CI # https://github.com/travis-ci/travis-ci/issues/9815 dist: xenial install: - pip install --upgrade pip setuptools wheel - pip install tox-travis # Travis by default only clones a 'shallow' repository with --depth=50. # When building the distribution packages, we use git to determine the # package version string (via setuptools_scm), hence we need to fetch # the whole repo, and not just the last 50 commits. - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then git fetch --unshallow; fi script: tox after_success: # if it's a tagged commit, upload distribution packages to PyPI - | if [ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_REPO_SLUG" == "typemytype/booleanOperations" ] && [ "$TRAVIS_PYTHON_VERSION" == "3.6" ]; then pip install --upgrade twine pip setuptools wheel python setup.py sdist pip wheel --no-deps --wheel-dir dist . twine upload dist/*.whl dist/*.zip fi deploy: # deploy to Github Releases on tags - provider: releases api_key: secure: H2rz0E/GjRrRmvf6EYWv7Fyu+y4qU+InGiYlQ6xoyrjmMFCTYiPNW0ysu8893JFILusfO48zzEix7HcumQXtJU16XYwqft9eiWIGzSlP1krusmpQ0yTnuy9wEQcd7fXLEQDfiVZpJxWpC1TtsuIdDqpSUFcHDg4hlvn1IIh+6HwRP6+M5Mprze087ydBCKkO4CvqR4olC7bkSPorRtqUsiUHlUxYFS+R75a8ghqVQbky7ew9/r90zljCBFwYTPEv9tQ7OxnAQiUaAYojceThsBrNP1lH83tOoqB2FkKr9I5dxi5gqNuUsJHh3QK0V59oQCxU+iVFYfrlIZHTBQ+2caxIeWm2k9RkMXXkbg1LyfpORt+5L+NcFNE4WT0DTlYrKnpW+jrak0LEWaI97+15uHahph3vOJgaZrTFvRQZUJTWCNcji2jGbZg+O3pS1Vtd1xEgJ1TcrkOYLYBMrVOywbuQL4sXofLxH3I5RwAenkgm5TwxtuiNxpUkqlEhzJ+GYwWtfb7/qtcLIK7Xn6cDdsQG6XYQMVt+HpCKgRLlrQC8si6l+jZBEtJ7o4IKeYbtybFCe5H3V0jnRFRyncPPdpBavqJ093Pr7fGLvmZuHl551WRtyPlBN4aMGsqvqUSjwr/TxsX5zZ1tQ4QTN2YTmCdeUO6P/fwTBf5p0F3QFo8= file_glob: true file: "dist/*" skip_cleanup: true on: repo: typemytype/booleanOperations tags: true all_branches: true python: 3.6 booleanOperations-0.9.0/LICENSE000066400000000000000000000020731356304034400162140ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Frederik Berlaen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. booleanOperations-0.9.0/Lib/000077500000000000000000000000001356304034400157135ustar00rootroot00000000000000booleanOperations-0.9.0/Lib/booleanOperations/000077500000000000000000000000001356304034400213765ustar00rootroot00000000000000booleanOperations-0.9.0/Lib/booleanOperations/__init__.py000066400000000000000000000007621356304034400235140ustar00rootroot00000000000000from .booleanOperationManager import BooleanOperationManager from .exceptions import BooleanOperationsError try: from ._version import version as __version__ except ImportError: __version__ = "0.0.0+unknown" # export BooleanOperationManager static methods union = BooleanOperationManager.union difference = BooleanOperationManager.difference intersection = BooleanOperationManager.intersection xor = BooleanOperationManager.xor getIntersections = BooleanOperationManager.getIntersections booleanOperations-0.9.0/Lib/booleanOperations/booleanGlyph.py000066400000000000000000000161011356304034400243720ustar00rootroot00000000000000import weakref from copy import deepcopy from fontTools.pens.pointPen import ( AbstractPointPen, PointToSegmentPen, SegmentToPointPen) from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.areaPen import AreaPen from .booleanOperationManager import BooleanOperationManager manager = BooleanOperationManager() class BooleanGlyphDataPointPen(AbstractPointPen): def __init__(self, glyph): self._glyph = glyph self._points = [] self.copyContourData = True def _flushContour(self): points = self._points if len(points) == 1 and points[0][0] == "move": # it's an anchor segmentType, pt, smooth, name = points[0] self._glyph.anchors.append((pt, name)) elif self.copyContourData: # ignore double points on start and end firstPoint = points[0] if firstPoint[0] == "move": # remove trailing off curves in an open path while points[-1][0] is None: points.pop() lastPoint = points[-1] if firstPoint[0] is not None and lastPoint[0] is not None: if firstPoint[1] == lastPoint[1]: if firstPoint[0] in ("line", "move"): del points[0] else: raise AssertionError("Unhandled point type sequence") elif firstPoint[0] == "move": # auto close the path _, pt, smooth, name = firstPoint points[0] = "line", pt, smooth, name contour = self._glyph.contourClass() contour._points = points self._glyph.contours.append(contour) def beginPath(self, identifier=None): self._points = [] def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): self._points.append((segmentType, pt, smooth, name)) def endPath(self): self._flushContour() def addComponent(self, baseGlyphName, transformation): self._glyph.components.append((baseGlyphName, transformation)) class BooleanContour: """ Contour like object. """ def __init__(self): self._points = [] self._clockwise = None self._bounds = None def __len__(self): return len(self._points) # shallow contour API def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def drawPoints(self, pointPen): pointPen.beginPath() for segmentType, pt, smooth, name in self._points: pointPen.addPoint(pt=pt, segmentType=segmentType, smooth=smooth, name=name) pointPen.endPath() def _get_clockwise(self): if self._clockwise is None: pen = AreaPen() pen.endPath = pen.closePath self.draw(pen) self._clockwise = pen.value < 0 return self._clockwise clockwise = property(_get_clockwise) def _get_bounds(self): if self._bounds is None: pen = BoundsPen(None) self.draw(pen) self._bounds = pen.bounds return self._bounds bounds = property(_get_bounds) class BooleanGlyph: """ Glyph like object handling boolean operations. union: result = BooleanGlyph(glyph).union(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) | BooleanGlyph(glyph2) difference: result = BooleanGlyph(glyph).difference(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) % BooleanGlyph(glyph2) intersection: result = BooleanGlyph(glyph).intersection(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) & BooleanGlyph(glyph2) xor: result = BooleanGlyph(glyph).xor(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) ^ BooleanGlyph(glyph2) """ contourClass = BooleanContour def __init__(self, glyph=None, copyContourData=True): self.contours = [] self.components = [] self.anchors = [] self.name = None self.unicodes = None self.width = None self.lib = {} self.note = None if glyph: pen = self.getPointPen() pen.copyContourData = copyContourData glyph.drawPoints(pen) self.name = glyph.name self.unicodes = glyph.unicodes self.width = glyph.width self.lib = deepcopy(glyph.lib) self.note = glyph.note if not isinstance(glyph, self.__class__): self.getSourceGlyph = weakref.ref(glyph) def __repr__(self): return "" % self.name def __len__(self): return len(self.contours) def __getitem__(self, index): return self.contours[index] def getSourceGlyph(self): return None def getParent(self): source = self.getSourceGlyph() if source: return source.getParent() return None # shalllow glyph API def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def drawPoints(self, pointPen): for contour in self.contours: contour.drawPoints(pointPen) for baseName, transformation in self.components: pointPen.addComponent(baseName, transformation) for pt, name in self.anchors: pointPen.beginPath() pointPen.addPoint(pt=pt, segmentType="move", smooth=False, name=name) pointPen.endPath() def getPen(self): return SegmentToPointPen(self.getPointPen()) def getPointPen(self): return BooleanGlyphDataPointPen(self) # boolean operations def _booleanMath(self, operation, other): if not isinstance(other, self.__class__): other = self.__class__(other) destination = self.__class__(self, copyContourData=False) func = getattr(manager, operation) if operation == "union": contours = self.contours if other is not None: contours += other.contours func(contours, destination.getPointPen()) else: subjectContours = self.contours clipContours = other.contours func(subjectContours, clipContours, destination.getPointPen()) return destination def __or__(self, other): return self.union(other) __ror__ = __ior__ = __or__ def __mod__(self, other): return self.difference(other) __rmod__ = __imod__ = __mod__ def __and__(self, other): return self.intersection(other) __rand__ = __iand__ = __and__ def __xor__(self, other): return self.xor(other) __rxor__ = __ixor__ = __xor__ def union(self, other): return self._booleanMath("union", other) def difference(self, other): return self._booleanMath("difference", other) def intersection(self, other): return self._booleanMath("intersection", other) def xor(self, other): return self._booleanMath("xor", other) def removeOverlap(self): return self._booleanMath("union", None) booleanOperations-0.9.0/Lib/booleanOperations/booleanOperationManager.py000066400000000000000000000116671356304034400265560ustar00rootroot00000000000000from .flatten import InputContour, OutputContour from .exceptions import ( InvalidSubjectContourError, InvalidClippingContourError, ExecutionError) import pyclipper """ General Suggestions: - Contours should only be sent here if they actually overlap. This can be checked easily using contour bounds. - Only perform operations on closed contours. - contours must have an on curve point - some kind of a log """ _operationMap = { "union": pyclipper.CT_UNION, "intersection": pyclipper.CT_INTERSECTION, "difference": pyclipper.CT_DIFFERENCE, "xor": pyclipper.CT_XOR, } _fillTypeMap = { "evenOdd": pyclipper.PFT_EVENODD, "nonZero": pyclipper.PFT_NONZERO, # we keep the misspelling for compatibility with earlier versions "noneZero": pyclipper.PFT_NONZERO, } def clipExecute(subjectContours, clipContours, operation, subjectFillType="nonZero", clipFillType="nonZero"): pc = pyclipper.Pyclipper() for i, subjectContour in enumerate(subjectContours): try: pc.AddPath(subjectContour, pyclipper.PT_SUBJECT) except pyclipper.ClipperException: # skip invalid paths with no area if pyclipper.Area(subjectContour) != 0: raise InvalidSubjectContourError("contour %d is invalid for clipping" % i) for j, clipContour in enumerate(clipContours): try: pc.AddPath(clipContour, pyclipper.PT_CLIP) except pyclipper.ClipperException: # skip invalid paths with no area if pyclipper.Area(clipContour) == 0: raise InvalidClippingContourError("contour %d is invalid for clipping" % j) bounds = pc.GetBounds() if (bounds.bottom, bounds.left, bounds.top, bounds.right) == (0, 0, 0, 0): # do nothing if there are no paths return [] try: solution = pc.Execute(_operationMap[operation], _fillTypeMap[subjectFillType], _fillTypeMap[clipFillType]) except pyclipper.ClipperException as exc: raise ExecutionError(exc) return [[tuple(p) for p in path] for path in solution] def _performOperation(operation, subjectContours, clipContours, outPen): # prep the contours subjectInputContours = [InputContour(contour) for contour in subjectContours if contour and len(contour) > 1] clipInputContours = [InputContour(contour) for contour in clipContours if contour and len(contour) > 1] inputContours = subjectInputContours + clipInputContours resultContours = clipExecute([subjectInputContour.originalFlat for subjectInputContour in subjectInputContours], [clipInputContour.originalFlat for clipInputContour in clipInputContours], operation, subjectFillType="nonZero", clipFillType="nonZero") # convert to output contours outputContours = [OutputContour(contour) for contour in resultContours] # re-curve entire contour for inputContour in inputContours: for outputContour in outputContours: if outputContour.final: continue if outputContour.reCurveFromEntireInputContour(inputContour): # the input is expired if a match was made, # so stop passing it to the outputs break # curve fit for outputContour in outputContours: outputContour.reCurveSubSegments(inputContours) # output the results for outputContour in outputContours: outputContour.drawPoints(outPen) return outputContours class BooleanOperationManager: @staticmethod def union(contours, outPen): return _performOperation("union", contours, [], outPen) @staticmethod def difference(subjectContours, clipContours, outPen): return _performOperation("difference", subjectContours, clipContours, outPen) @staticmethod def intersection(subjectContours, clipContours, outPen): return _performOperation("intersection", subjectContours, clipContours, outPen) @staticmethod def xor(subjectContours, clipContours, outPen): return _performOperation("xor", subjectContours, clipContours, outPen) @staticmethod def getIntersections(contours): from .flatten import _scalePoints, inverseClipperScale # prep the contours inputContours = [InputContour(contour) for contour in contours if contour and len(contour) > 1] inputFlatPoints = set() for contour in inputContours: inputFlatPoints.update(contour.originalFlat) resultContours = clipExecute( [inputContour.originalFlat for inputContour in inputContours], [], "union", subjectFillType="nonZero", clipFillType="nonZero") resultFlatPoints = set() for contour in resultContours: resultFlatPoints.update(contour) intersections = resultFlatPoints - inputFlatPoints return _scalePoints(intersections, inverseClipperScale) booleanOperations-0.9.0/Lib/booleanOperations/exceptions.py000066400000000000000000000013521356304034400241320ustar00rootroot00000000000000class BooleanOperationsError(Exception): """Base BooleanOperations exception""" class UnsupportedContourError(BooleanOperationsError): """Raised when asked to perform an operation on an unsupported curve type.""" class InvalidContourError(BooleanOperationsError): """Raised when any input contour is invalid""" class InvalidSubjectContourError(InvalidContourError): """Raised when a 'subject' contour is not valid""" class InvalidClippingContourError(InvalidContourError): """Raised when a 'clipping' contour is not valid""" class OpenContourError(BooleanOperationsError): """Raised when any input contour is open""" class ExecutionError(BooleanOperationsError): """Raised when clipping execution fails""" booleanOperations-0.9.0/Lib/booleanOperations/flatten.py000066400000000000000000001361641356304034400234200ustar00rootroot00000000000000import math from fontTools.misc import bezierTools from fontTools.pens.basePen import decomposeQuadraticSegment import pyclipper from .exceptions import OpenContourError, UnsupportedContourError """ To Do: - the stuff listed below - need to know what kind of curves should be used for curve fit--curve or qcurve - false curves and duplicate points need to be filtered early on notes: - the flattened segments *must* be cyclical. if they aren't, matching is almost impossible. optimization ideas: - the flattening of the output segment in the full contour matching is probably expensive. - there should be a way to flag an input contour as entirely used so that it isn't tried and tried and tried for segment matches. - do a faster test when matching segments: when a end match is found, jump back input length and grab the output segment. test for match with the input. - cache input contour objects. matching these to incoming will be a little difficult because of point names and identifiers. alternatively, deal with those after the fact. - some tests on input before conversion to input objects could yield significant speedups. would need to check each contour for self intersection and each non-self-intersectingcontour for collision with other contours. and contours that don't have a hit could be skipped. this cound be done roughly with bounds. this should probably be done by extenal callers. - set a proper starting points of the output segments based on known points known points are: input oncurve points if nothing found intersection points (only use this is in the final curve fitting stage) test cases: - untouched contour: make clockwise and counter-clockwise tests of the same contour """ epsilon = 1e-8 # factors for transferring coordinates to and from Clipper clipperScale = 1 << 17 inverseClipperScale = 1.0 / clipperScale # approximateSegmentLength setting _approximateSegmentLength = 5.3 # ------------- # Input Objects # ------------- # Input class InputContour: def __init__(self, contour): # gather the point data pointPen = ContourPointDataPen() contour.drawPoints(pointPen) points = pointPen.getData() reversedPoints = _reversePoints(points) # gather segments self.segments = _convertPointsToSegments(points) # only calculate once all the flat points. # it seems to have some tiny difference and its a lot faster # if the flat points are calculated from the reversed input points. self.reversedSegments = _convertPointsToSegments(reversedPoints, willBeReversed=True) # simple reverse the flat points and store them in the reversedSegments index = 0 for segment in self.segments: otherSegment = self.reversedSegments[index] otherSegment.flat = segment.getReversedFlatPoints() index -= 1 # get the direction; returns True if counter-clockwise, False otherwise self.clockwise = not pyclipper.Orientation(points) # store the gathered data if self.clockwise: self.clockwiseSegments = self.segments self.counterClockwiseSegments = self.reversedSegments else: self.clockwiseSegments = self.reversedSegments self.counterClockwiseSegments = self.segments # flag indicating if the contour has been used self.used = False # ---------- # Attributes # ---------- # the original direction in flat segments def _get_originalFlat(self): if self.clockwise: return self.clockwiseFlat else: return self.counterClockwiseFlat originalFlat = property(_get_originalFlat) # the clockwise direction in flat segments def _get_clockwiseFlat(self): flat = [] segments = self.clockwiseSegments for segment in segments: flat.extend(segment.flat) return flat clockwiseFlat = property(_get_clockwiseFlat) # the counter-clockwise direction in flat segments def _get_counterClockwiseFlat(self): flat = [] segments = self.counterClockwiseSegments for segment in segments: flat.extend(segment.flat) return flat counterClockwiseFlat = property(_get_counterClockwiseFlat) def hasOnCurve(self): for inputSegment in self.segments: if not inputSegment.used and inputSegment.segmentType != "line": return True return False class InputSegment: # __slots__ = ["points", "previousOnCurve", "scaledPreviousOnCurve", "flat", "used"] def __init__(self, points=None, previousOnCurve=None, willBeReversed=False): if points is None: points = [] self.points = points self.previousOnCurve = previousOnCurve self.scaledPreviousOnCurve = _scaleSinglePoint(previousOnCurve, scale=clipperScale) self.used = False self.flat = [] # if the bcps are equal to the oncurves convert the segment to a line segment. # otherwise this causes an error when flattening. if self.segmentType == "curve": if previousOnCurve == points[0].coordinates and points[1].coordinates == points[-1].coordinates: oncurve = points[-1] oncurve.segmentType = "line" self.points = points = [oncurve] elif previousOnCurve[0] == points[0].coordinates[0] == points[1].coordinates[0] == points[-1].coordinates[0]: oncurve = points[-1] oncurve.segmentType = "line" self.points = points = [oncurve] elif previousOnCurve[1] == points[0].coordinates[1] == points[1].coordinates[1] == points[-1].coordinates[1]: oncurve = points[-1] oncurve.segmentType = "line" self.points = points = [oncurve] # its a reversed segment the flat points will be set later on in the InputContour if willBeReversed: return pointsToFlatten = [] if self.segmentType == "qcurve": assert len(points) >= 0 flat = [] currentOnCurve = previousOnCurve pointCoordinates = [point.coordinates for point in points] for pt1, pt2 in decomposeQuadraticSegment(pointCoordinates[1:]): pt0x, pt0y = currentOnCurve pt1x, pt1y = pt1 pt2x, pt2y = pt2 mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x) mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y) mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x) mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y) convertedQuadPointToFlatten = [currentOnCurve, (mid1x, mid1y), (mid2x, mid2y), pt2] flat.extend(_flattenSegment(convertedQuadPointToFlatten)) currentOnCurve = pt2 self.flat = flat # this shoudl be easy. # copy the quad to cubic from fontTools.pens.basePen elif self.segmentType == "curve": pointsToFlatten = [previousOnCurve] + [point.coordinates for point in points] else: assert len(points) == 1 self.flat = [point.coordinates for point in points] if pointsToFlatten: self.flat = _flattenSegment(pointsToFlatten) # if len(self.flat) == 1 and self.segmentType == "curve": # oncurve = self.points[-1] # oncurve.segmentType = "line" # self.points = [oncurve] self.flat = _scalePoints(self.flat, scale=clipperScale) self.flat = _checkFlatPoints(self.flat) self.used = False def _get_segmentType(self): return self.points[-1].segmentType segmentType = property(_get_segmentType) def getReversedFlatPoints(self): reversedFlatPoints = [self.scaledPreviousOnCurve] + self.flat[:-1] reversedFlatPoints.reverse() return reversedFlatPoints def split(self, tValues): """ Split the segment according the t values """ if self.segmentType == "curve": on1 = self.previousOnCurve off1 = self.points[0].coordinates off2 = self.points[1].coordinates on2 = self.points[2].coordinates return bezierTools.splitCubicAtT(on1, off1, off2, on2, *tValues) elif self.segmentType == "line": segments = [] x1, y1 = self.previousOnCurve x2, y2 = self.points[0].coordinates dx = x2 - x1 dy = y2 - y1 pp = x1, y1 for t in tValues: np = (x1+dx*t, y1+dy*t) segments.append([pp, np]) pp = np segments.append([pp, (x2, y2)]) return segments elif self.segmentType == "qcurve": raise NotImplementedError else: raise NotImplementedError def tValueForPoint(self, point): """ get a t values for a given point required: the point must be a point on the curve. in an overlap cause the point will be an intersection points wich is alwasy a point on the curve """ if self.segmentType == "curve": on1 = self.previousOnCurve off1 = self.points[0].coordinates off2 = self.points[1].coordinates on2 = self.points[2].coordinates return _tValueForPointOnCubicCurve(point, (on1, off1, off2, on2)) elif self.segmentType == "line": return _tValueForPointOnLine(point, (self.previousOnCurve, self.points[0].coordinates)) elif self.segmentType == "qcurve": raise NotImplementedError else: raise NotImplementedError class InputPoint: __slots__ = ["coordinates", "segmentType", "smooth", "name", "kwargs"] def __init__(self, coordinates, segmentType=None, smooth=False, name=None, kwargs=None): x, y = coordinates self.coordinates = coordinates self.segmentType = segmentType self.smooth = smooth self.name = name self.kwargs = kwargs def __getitem__(self, i): return self.coordinates[i] def copy(self): copy = self.__class__( coordinates=self.coordinates, segmentType=self.segmentType, smooth=self.smooth, name=self.name, kwargs=self.kwargs ) return copy def __str__(self): return f"{self.segmentType} {self.coordinates}" def __repr__(self): return self.__str__() # ------------- # Input Support # ------------- class ContourPointDataPen: """ Point pen for gathering raw contour data. An instance of this pen may only be used for one contour. """ def __init__(self): self._points = None self._foundStartingPoint = False def getData(self): """ Return a list of normalized InputPoint objects for the contour drawn with this pen. """ # organize the points into segments # 1. make sure there is an on curve haveOnCurve = False for point in self._points: if point.segmentType is not None: haveOnCurve = True break # 2. move the off curves to front of the list if haveOnCurve: _prepPointsForSegments(self._points) # 3. ignore double points on start and end firstPoint = self._points[0] lastPoint = self._points[-1] if firstPoint.segmentType is not None and lastPoint.segmentType is not None: if firstPoint.coordinates == lastPoint.coordinates: if (firstPoint.segmentType in ["line", "move"]): del self._points[0] else: raise AssertionError("Unhandled point type sequence") # done return self._points def beginPath(self, **kwargs): # TODO store and pass on the contour identifier? assert self._points is None self._points = [] def endPath(self): pass def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): if segmentType == "move": raise OpenContourError("Unhandled open contour") if not self._foundStartingPoint and segmentType is not None: kwargs['startingPoint'] = self._foundStartingPoint = True data = InputPoint( coordinates=pt, segmentType=segmentType, smooth=smooth, name=name, kwargs=kwargs ) self._points.append(data) def addComponent(self, baseGlyphName, transformation): raise NotImplementedError def _prepPointsForSegments(points): """ Move any off curves at the end of the contour to the beginning of the contour. This makes segmentation easier. """ while 1: point = points[-1] if point.segmentType: break else: point = points.pop() points.insert(0, point) continue break def _copyPoints(points): """ Make a shallow copy of the points. """ copied = [point.copy() for point in points] return copied def _reversePoints(points): """ Reverse the points. This differs from the reversal point pen in RoboFab in that it doesn't worry about maintaing the start point position. That has no benefit within the context of this module. """ # copy the points points = _copyPoints(points) # find the first on curve type and recycle # it for the last on curve type firstOnCurve = None for index, point in enumerate(points): if point.segmentType is not None: firstOnCurve = index break lastSegmentType = points[firstOnCurve].segmentType # reverse the points points = reversed(points) # work through the reversed remaining points final = [] for point in points: segmentType = point.segmentType if segmentType is not None: point.segmentType = lastSegmentType lastSegmentType = segmentType final.append(point) # move any offcurves at the end of the points # to the start of the points _prepPointsForSegments(final) # done return final def _convertPointsToSegments(points, willBeReversed=False): """ Compile points into InputSegment objects. """ # get the last on curve previousOnCurve = None for point in reversed(points): if point.segmentType is not None: previousOnCurve = point.coordinates break assert previousOnCurve is not None # gather the segments offCurves = [] segments = [] for point in points: # off curve, hold. if point.segmentType is None: offCurves.append(point) elif point.segmentType in {"curve", "line"}: segment = InputSegment( points=offCurves + [point], previousOnCurve=previousOnCurve, willBeReversed=willBeReversed ) segments.append(segment) offCurves = [] previousOnCurve = point.coordinates else: raise UnsupportedContourError( "Trying to perform operation on unsupported segment type.", point.segmentType ) assert not offCurves return segments # -------------- # Output Objects # -------------- class OutputContour: def __init__(self, pointList): if pointList[0] == pointList[-1]: del pointList[-1] self.clockwise = not pyclipper.Orientation(pointList) self.segments = [ OutputSegment( segmentType="flat", points=[point] ) for point in pointList ] def _scalePoint(self, point): x, y = point x = x * inverseClipperScale if int(x) == x: x = int(x) y = y * inverseClipperScale if int(y) == y: y = int(y) return x, y # ---------- # Attributes # ---------- def _get_final(self): # XXX this could be optimized: # store a fixed value after teh contour is finalized # don't do the dymanic searching if that flag is set to True for segment in self.segments: if not segment.final: return False return True final = property(_get_final) # -------------------------- # Re-Curve and Curve Fitting # -------------------------- def reCurveFromEntireInputContour(self, inputContour): """ Match if entire input contour matches entire output contour, allowing for different start point. """ if self.clockwise: inputFlat = inputContour.clockwiseFlat else: inputFlat = inputContour.counterClockwiseFlat outputFlat = [] for segment in self.segments: # XXX this could be expensive assert segment.segmentType == "flat" outputFlat += segment.points # test lengths haveMatch = False if len(inputFlat) == len(outputFlat): if inputFlat == outputFlat: haveMatch = True else: inputStart = inputFlat[0] if inputStart in outputFlat: # there should be only one occurance of the point # but handle it just in case if outputFlat.count(inputStart) > 1: startIndexes = [index for index, point in enumerate(outputFlat) if point == inputStart] else: startIndexes = [outputFlat.index(inputStart)] # slice and dice to test possible orders for startIndex in startIndexes: test = outputFlat[startIndex:] + outputFlat[:startIndex] if inputFlat == test: haveMatch = True break if haveMatch: # clear out the flat points self.segments = [] # replace with the appropriate points from the input if self.clockwise: inputSegments = inputContour.clockwiseSegments else: inputSegments = inputContour.counterClockwiseSegments for inputSegment in inputSegments: self.segments.append( OutputSegment( segmentType=inputSegment.segmentType, points=[ OutputPoint( coordinates=point.coordinates, segmentType=point.segmentType, smooth=point.smooth, name=point.name, kwargs=point.kwargs ) for point in inputSegment.points ], final=True ) ) inputSegment.used = True # reset the direction of the final contour self.clockwise = inputContour.clockwise return True return False def reCurveSubSegmentsCheckInputContoursOnHasCurve(self, inputContours): # test is the remaining input contours contains only lineTo points # XXX could be cached return True # for inputContour in inputContours: # if inputContour.used: # continue # if inputContour.hasOnCurve(): # return True # return False def reCurveSubSegments(self, inputContours): if not self.segments: # its all done return # the inputContours has some curved segments # if not it all the segments will be converted at the end if self.reCurveSubSegmentsCheckInputContoursOnHasCurve(inputContours): # collect all flat points in a dict of unused inputContours # collect both clockwise segment and counterClockwise segments # it happens a lot that the directions turns around # the clockwise attribute can help but testing the directions is always needed # collect all oncurve points as well flatInputPointsSegmentDict = dict() reversedFlatInputPointsSegmentDict = dict() flatIntputOncurves = set() for inputContour in inputContours: if inputContour.used: continue if self.clockwise: inputSegments = inputContour.clockwiseSegments reversedSegments = inputContour.counterClockwiseSegments else: inputSegments = inputContour.counterClockwiseSegments reversedSegments = inputContour.clockwiseSegments for inputSegment in inputSegments: if inputSegment.used: continue for p in inputSegment.flat: flatInputPointsSegmentDict[p] = inputSegment flatIntputOncurves.add(inputSegment.scaledPreviousOnCurve) for inputSegment in reversedSegments: if inputSegment.used: continue for p in inputSegment.flat: reversedFlatInputPointsSegmentDict[p] = inputSegment flatIntputOncurves.add(inputSegment.scaledPreviousOnCurve) flatInputPoints = set(flatInputPointsSegmentDict.keys()) # reset the starting point to a known point. # not somewhere in the middle of a flatten point list firstSegment = self.segments[0] foundStartingPoint = True if firstSegment.segmentType == "flat": foundStartingPoint = False for index, segment in enumerate(self.segments): if segment.segmentType in ["line", "curve", "qcurve"]: foundStartingPoint = True break if foundStartingPoint: # if found re index the segments # if there is no known starting point found do it later based on the intersection points self.segments = self.segments[index:] + self.segments[:index] # collect all flat points in a intersect segment remainingSubSegment = OutputSegment(segmentType="intersect", points=[]) # store all segments in one big temp list newSegments = [] for index, segment in enumerate(self.segments): if segment.segmentType != "flat": # when the segment contains only one points its a line cause it is a single intersection point if len(remainingSubSegment.points) == 1: remainingSubSegment.segmentType = "line" remainingSubSegment.final = True remainingSubSegment.points = [ OutputPoint( coordinates=self._scalePoint(point), segmentType="line", smooth=point.smooth, name=point.name, kwargs=point.kwargs ) for point in remainingSubSegment.points ] newSegments.append(remainingSubSegment) remainingSubSegment = OutputSegment(segmentType="intersect", points=[]) newSegments.append(segment) continue remainingSubSegment.points.extend(segment.points) newSegments.append(remainingSubSegment) # loop over all segments for segment in newSegments: # handle only segments tagged as intersect if segment.segmentType != "intersect": continue # skip empty segments if not segment.points: continue # get al inputSegments, this is an unorderd list of all points no in the the flatInputPoints segmentPointsSet = set(segment.points) intersectionPoints = segmentPointsSet - flatInputPoints # merge both oncurves and intersectionPoints as known points possibleStartingPoints = flatIntputOncurves | intersectionPoints hasOncurvePoints = segmentPointsSet & flatIntputOncurves # if not starting point is found earlier do it here foundStartingPointIndex = None if not foundStartingPoint: for index, p in enumerate(segment.points): if p in flatIntputOncurves: foundStartingPointIndex = index break if foundStartingPointIndex is None: for index, p in enumerate(segment.points): if p in intersectionPoints: foundStartingPointIndex = index break segment.points = segment.points[foundStartingPointIndex:] + segment.points[:foundStartingPointIndex] # split list based on oncurvepoints and intersection points, aka possibleStartingPoints. segmentedFlatPoints = [[]] for p in segment.points: segmentedFlatPoints[-1].append(p) if p in possibleStartingPoints: segmentedFlatPoints.append([]) if not segmentedFlatPoints[-1]: segmentedFlatPoints.pop(-1) if len(segmentedFlatPoints) > 1 and len(segmentedFlatPoints[0]) == 1: # if last segment is a curve, the start point may be last point on the last segment. If so, merge them. # check if they both have the same inputSegment or reversedInputSegment fp = segmentedFlatPoints[0][0] lp = segmentedFlatPoints[-1][-1] mergeFirstSegments = False if fp in flatInputPoints and lp in flatInputPoints: firstInputSegment = flatInputPointsSegmentDict[fp] lastInputSegment = flatInputPointsSegmentDict[lp] reversedFirstInputSegment = reversedFlatInputPointsSegmentDict[fp] reversedLastInputSegment = reversedFlatInputPointsSegmentDict[lp] if (firstInputSegment.segmentType == reversedFirstInputSegment.segmentType == "curve") or (lastInputSegment.segmentType == reversedLastInputSegment.segmentType == "curve"): if firstInputSegment == lastInputSegment or reversedFirstInputSegment == reversedLastInputSegment: mergeFirstSegments = True # elif len(firstInputSegment.points) > 1 and len(lastInputSegment.points) > 1: elif fp == lastInputSegment.scaledPreviousOnCurve: mergeFirstSegments = True elif lp == firstInputSegment.scaledPreviousOnCurve: mergeFirstSegments = True elif fp == reversedLastInputSegment.scaledPreviousOnCurve: mergeFirstSegments = True elif lp == reversedFirstInputSegment.scaledPreviousOnCurve: mergeFirstSegments = True elif not hasOncurvePoints and _distance(fp, lp): # Merge last segment with first segment if the distance between the last point and the first # point is less than the step distance between the last two points. _approximateSegmentLength # can be significantly smaller than this step size. if len(segmentedFlatPoints[-1]) > 1: f1 = segmentedFlatPoints[-1][-2] f2 = segmentedFlatPoints[-1][-1] stepLen = _distance(f1, f2) else: stepLen = _approximateSegmentLength*clipperScale if _distance(fp, lp) <= stepLen: mergeFirstSegments = True if mergeFirstSegments: segmentedFlatPoints[0] = segmentedFlatPoints[-1] + segmentedFlatPoints[0] segmentedFlatPoints.pop(-1) mergeFirstSegments = False convertedSegments = [] previousIntersectionPoint = None if segmentedFlatPoints[-1][-1] in intersectionPoints: previousIntersectionPoint = self._scalePoint(segmentedFlatPoints[-1][-1]) elif segmentedFlatPoints[0][0] in intersectionPoints: previousIntersectionPoint = self._scalePoint(segmentedFlatPoints[0][0]) for flatSegment in segmentedFlatPoints: # search two points in the flat segment that is not an inputOncurve or intersection point # to get a proper direction of the flatSegment # based on these two points pick a inputSegment fp = ep = None for p in flatSegment: if p in possibleStartingPoints: continue elif fp is None: fp = p elif ep is None: ep = p else: break canDoFastLine = True if ep is None and ((fp is None) or (len(flatSegment) == 2)): # if fp is not None, then it is a flattened part of a curve, and should be used to derive the input segment. # It may be either the first or second point. # If fp is None, I use the original logic. if fp is None: fp = flatSegment[-1] # flat segment only contains two intersection points or one intersection point and one input oncurve point # this can be ignored cause this is a very small line # and will be converted to a simple line if self.clockwise: inputSegment = reversedFlatInputPointsSegmentDict.get(fp) else: inputSegment = flatInputPointsSegmentDict.get(fp) else: # get inputSegment based on the clockwise settings inputSegment = flatInputPointsSegmentDict[fp] if ep is not None and ep in inputSegment.flat: # if two points are found get indexes fi = inputSegment.flat.index(fp) ei = inputSegment.flat.index(ep) if fi > ei: # if the start index is bigger # get the reversed inputSegment inputSegment = reversedFlatInputPointsSegmentDict[fp] else: # if flat segment is short and has only one point not in intersections and input oncurves # test it against the reversed inputSegment reversedInputSegment = reversedFlatInputPointsSegmentDict[fp] if flatSegment[0] == reversedInputSegment.flat[0] and flatSegment[-1] == reversedInputSegment.flat[-1]: inputSegment = reversedInputSegment elif flatSegment[0] in intersectionPoints and flatSegment[-1] == reversedInputSegment.flat[-1]: inputSegment = reversedInputSegment elif flatSegment[-1] in intersectionPoints and flatSegment[0] == reversedInputSegment.flat[0]: inputSegment = reversedInputSegment canDoFastLine = False # if there is only one point in a flat segment # this is a single intersection points (two crossing lineTo's) if inputSegment.segmentType == "curve": canDoFastLine = False if (len(flatSegment) == 1 or inputSegment is None) and canDoFastLine: # p = flatSegment[0] for p in flatSegment: previousIntersectionPoint = self._scalePoint(p) pointInfo = dict() kwargs = dict() if p in flatInputPointsSegmentDict: lineSegment = flatInputPointsSegmentDict[p] segmentPoint = lineSegment.points[-1] pointInfo["smooth"] = segmentPoint.smooth pointInfo["name"] = segmentPoint.name kwargs.update(segmentPoint.kwargs) convertedSegments.append(OutputPoint(coordinates=previousIntersectionPoint, segmentType="line", kwargs=kwargs, **pointInfo)) continue tValues = None lastPointWithAttributes = None if flatSegment[0] == inputSegment.flat[0] and flatSegment[-1] != inputSegment.flat[-1]: # needed the first part of the segment # if previousIntersectionPoint is None: # previousIntersectionPoint = self._scalePoint(flatSegment[-1]) searchPoint = self._scalePoint(flatSegment[-1]) tValues = inputSegment.tValueForPoint(searchPoint) curveNeeded = 0 replacePointOnNewCurve = [(3, searchPoint)] previousIntersectionPoint = searchPoint elif flatSegment[-1] == inputSegment.flat[-1] and flatSegment[0] != inputSegment.flat[0]: # needed the end of the segment if previousIntersectionPoint is None: previousIntersectionPoint = self._scalePoint(flatSegment[0]) convertedSegments.append(OutputPoint( coordinates=previousIntersectionPoint, segmentType="line", )) tValues = inputSegment.tValueForPoint(previousIntersectionPoint) curveNeeded = -1 replacePointOnNewCurve = [(0, previousIntersectionPoint)] previousIntersectionPoint = None lastPointWithAttributes = inputSegment.points[-1] elif flatSegment[0] != inputSegment.flat[0] and flatSegment[-1] != inputSegment.flat[-1]: # needed the a middle part of the segment if previousIntersectionPoint is None: previousIntersectionPoint = self._scalePoint(flatSegment[0]) tValues = inputSegment.tValueForPoint(previousIntersectionPoint) searchPoint = self._scalePoint(flatSegment[-1]) tValues.extend(inputSegment.tValueForPoint(searchPoint)) curveNeeded = 1 replacePointOnNewCurve = [(0, previousIntersectionPoint), (3, searchPoint)] previousIntersectionPoint = searchPoint else: # take the whole segments as is newCurve = [ OutputPoint( coordinates=point.coordinates, segmentType=point.segmentType, smooth=point.smooth, name=point.name, kwargs=point.kwargs ) for point in inputSegment.points ] convertedSegments.extend(newCurve) previousIntersectionPoint = None # if we found some tvalue, split the curve and get the requested parts of the splitted curves if tValues: newCurve = inputSegment.split(tValues) newCurve = list(newCurve[curveNeeded]) for i, replace in replacePointOnNewCurve: newCurve[i] = replace newCurve = [OutputPoint(coordinates=p, segmentType=None) for p in newCurve[1:]] newCurve[-1].segmentType = inputSegment.segmentType if lastPointWithAttributes is not None: newCurve[-1].smooth = lastPointWithAttributes.smooth newCurve[-1].name = lastPointWithAttributes.name newCurve[-1].kwargs = lastPointWithAttributes.kwargs convertedSegments.extend(newCurve) # replace the the points with the converted segments segment.points = convertedSegments segment.segmentType = "reCurved" self.segments = newSegments # XXX convert all of the remaining segments to lines for segment in self.segments: if not segment.points: continue if segment.segmentType not in ["intersect", "flat"]: continue segment.segmentType = "line" segment.points = [ OutputPoint( coordinates=self._scalePoint(point), segmentType="line", # smooth=point.smooth, # name=point.name, # kwargs=point.kwargs ) for point in segment.points ] # ---- # Draw # ---- def drawPoints(self, pointPen): pointPen.beginPath() points = [] for segment in self.segments: points.extend(segment.points) hasOnCurve = False originalStartingPoints = [] for index, point in enumerate(points): if point.segmentType is not None: hasOnCurve = True if point.kwargs is not None and point.kwargs.get("startingPoint"): distanceFromOrigin = math.hypot(*point) originalStartingPoints.append((distanceFromOrigin, index)) if originalStartingPoints: # use the original starting point that is closest to the origin startingPointIndex = sorted(originalStartingPoints)[0][1] points = points[startingPointIndex:] + points[:startingPointIndex] elif hasOnCurve: while points[0].segmentType is None: p = points.pop(0) points.append(p) previousPointCoordinates = None for point in points: if previousPointCoordinates is not None and point.segmentType and tuple(point.coordinates) == previousPointCoordinates: continue kwargs = {} if point.kwargs is not None: kwargs = point.kwargs pointPen.addPoint( point.coordinates, segmentType=point.segmentType, smooth=point.smooth, name=point.name, **kwargs ) if point.segmentType: previousPointCoordinates = tuple(point.coordinates) else: previousPointCoordinates = None pointPen.endPath() class OutputSegment: __slots__ = ["segmentType", "points", "final"] def __init__(self, segmentType=None, points=None, final=False): self.segmentType = segmentType if points is None: points = [] self.points = points self.final = final class OutputPoint(InputPoint): pass # ---------- # Misc. Math # ---------- def _tValueForPointOnCubicCurve(point, cubicCurve, isHorizontal=0): """ Finds a t value on a curve from a point. The points must be originaly be a point on the curve. This will only back trace the t value, needed to split the curve in parts """ pt1, pt2, pt3, pt4 = cubicCurve a, b, c, d = bezierTools.calcCubicParameters(pt1, pt2, pt3, pt4) solutions = bezierTools.solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - point[isHorizontal]) solutions = [t for t in solutions if 0 <= t < 1] if not solutions and not isHorizontal: # can happen that a horizontal line doens intersect, try the vertical return _tValueForPointOnCubicCurve(point, (pt1, pt2, pt3, pt4), isHorizontal=1) if len(solutions) > 1: intersectionLenghts = {} for t in solutions: tp = _getCubicPoint(t, pt1, pt2, pt3, pt4) dist = _distance(tp, point) intersectionLenghts[dist] = t minDist = min(intersectionLenghts.keys()) solutions = [intersectionLenghts[minDist]] return solutions def _tValueForPointOnQuadCurve(point, pts, isHorizontal=0): quadSegments = decomposeQuadraticSegment(pts[1:]) previousOnCurve = pts[0] solutionsDict = dict() for index, (pt1, pt2) in enumerate(quadSegments): a, b, c = bezierTools.calcQuadraticParameters(previousOnCurve, pt1, pt2) subSolutions = bezierTools.solveQuadratic(a[isHorizontal], b[isHorizontal], c[isHorizontal] - point[isHorizontal]) subSolutions = [t for t in subSolutions if 0 <= t < 1] for t in subSolutions: solutionsDict[(t, index)] = _getQuadPoint(t, previousOnCurve, pt1, pt2) previousOnCurve = pt2 solutions = list(solutionsDict.keys()) if not solutions and not isHorizontal: return _tValueForPointOnQuadCurve(point, pts, isHorizontal=1) if len(solutions) > 1: intersectionLenghts = {} for t in solutions: tp = solutionsDict[t] dist = _distance(tp, point) intersectionLenghts[dist] = t minDist = min(intersectionLenghts.keys()) solutions = [intersectionLenghts[minDist]] return solutions def _tValueForPointOnLine(point, line): pt1, pt2 = line dist = _distance(pt1, point) totalDist = _distance(pt1, pt2) return [dist / totalDist] def _scalePoints(points, scale=1, convertToInteger=True): """ Scale points and optionally convert them to integers. """ if convertToInteger: points = [ (int(round(x * scale)), int(round(y * scale))) for (x, y) in points ] else: points = [(x * scale, y * scale) for (x, y) in points] return points def _scaleSinglePoint(point, scale=1, convertToInteger=True): """ Scale a single point """ x, y = point if convertToInteger: return int(round(x * scale)), int(round(y * scale)) else: return (x * scale, y * scale) def _intPoint(pt): return int(round(pt[0])), int(round(pt[1])) def _checkFlatPoints(points): _points = [] previousX = previousY = None for x, y in points: if x == previousX: continue elif y == previousY: continue if (x, y) not in _points: # is it possible that two flat point are on top of eachother??? _points.append((x, y)) previousX, previousY = x, y if _points[-1] != points[-1]: _points[-1] = points[-1] return _points """ The curve flattening code was forked and modified from RoboFab's FilterPen. That code was written by Erik van Blokland. """ def _flattenSegment(segment, approximateSegmentLength=_approximateSegmentLength): """ Flatten the curve segment int a list of points. The first and last points in the segment must be on curves. The returned list of points will not include the first on curve point. false curves (where the off curves are not any different from the on curves) must not be sent here. duplicate points must not be sent here. """ onCurve1, offCurve1, offCurve2, onCurve2 = segment if _pointOnLine(onCurve1, onCurve2, offCurve1) and _pointOnLine(onCurve1, onCurve2, offCurve2): return [onCurve2] est = _estimateCubicCurveLength(onCurve1, offCurve1, offCurve2, onCurve2) / approximateSegmentLength flat = [] minStep = 0.1564 step = 1.0 / est if step > .3: step = minStep t = step while t < 1: pt = _getCubicPoint(t, onCurve1, offCurve1, offCurve2, onCurve2) # ignore when point is in the same direction as the on - off curve line if not _pointOnLine(offCurve2, onCurve2, pt) and not _pointOnLine(onCurve1, offCurve1, pt): flat.append(pt) t += step flat.append(onCurve2) return flat def _distance(pt1, pt2): return math.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) def _pointOnLine(pt1, pt2, a): return abs(_distance(pt1, a) + _distance(a, pt2) - _distance(pt1, pt2)) < epsilon def _estimateCubicCurveLength(pt0, pt1, pt2, pt3, precision=10): """ Estimate the length of this curve by iterating through it and averaging the length of the flat bits. """ points = [] length = 0 step = 1.0 / precision factors = range(0, precision + 1) for i in factors: points.append(_getCubicPoint(i * step, pt0, pt1, pt2, pt3)) for i in range(len(points) - 1): pta = points[i] ptb = points[i + 1] length += _distance(pta, ptb) return length def _mid(pt1, pt2): """ (Point, Point) -> Point Return the point that lies in between the two input points. """ (x0, y0), (x1, y1) = pt1, pt2 return 0.5 * (x0 + x1), 0.5 * (y0 + y1) def _getCubicPoint(t, pt0, pt1, pt2, pt3): if t == 0: return pt0 if t == 1: return pt3 if t == 0.5: a = _mid(pt0, pt1) b = _mid(pt1, pt2) c = _mid(pt2, pt3) d = _mid(a, b) e = _mid(b, c) return _mid(d, e) else: cx = (pt1[0] - pt0[0]) * 3.0 cy = (pt1[1] - pt0[1]) * 3.0 bx = (pt2[0] - pt1[0]) * 3.0 - cx by = (pt2[1] - pt1[1]) * 3.0 - cy ax = pt3[0] - pt0[0] - cx - bx ay = pt3[1] - pt0[1] - cy - by t3 = t ** 3 t2 = t * t x = ax * t3 + bx * t2 + cx * t + pt0[0] y = ay * t3 + by * t2 + cy * t + pt0[1] return x, y def _getQuadPoint(t, pt0, pt1, pt2): if t == 0: return pt0 if t == 1: return pt2 else: cx = pt0[0] cy = pt0[1] bx = (pt1[0] - cx) * 2.0 by = (pt1[1] - cy) * 2.0 ax = pt2[0] - cx - bx ay = pt2[1] - cy - by x = ax * t**2 + bx * t + cx y = ay * t**2 + by * t + cy return x, y booleanOperations-0.9.0/MANIFEST.in000066400000000000000000000000531356304034400167410ustar00rootroot00000000000000exclude .travis.yml appveyor.yml .gitignorebooleanOperations-0.9.0/README.rst000066400000000000000000000115061356304034400166770ustar00rootroot00000000000000|Build Status| |PyPI| |Python Versions| BooleanOperations ================= Boolean operations on paths which uses a super fast `polygon clipper library by Angus Johnson `__. You can download the latest version from PyPI: https://pypi.org/project/booleanOperations. Install ------- `Pip `__ is the recommended tool to install booleanOperations. To install the latest version: .. code:: sh pip install booleanOperations BooleanOperations depends on the following packages: - `pyclipper `__: Cython wrapper for the C++ Clipper library - `fonttools `__ All dependencies are available on PyPI, so they will be resolved automatically upon installing booleanOperations. BooleanOperationManager ----------------------- Containing a ``BooleanOperationManager`` handling all boolean operations on paths. Paths must be similar to ``defcon``, ``robofab`` contours. A manager draws the result in a ``pointPen``. .. code:: py from booleanOperations import BooleanOperationManager manager = BooleanOperationManager() BooleanOperationManager() ~~~~~~~~~~~~~~~~~~~~~~~~~ Create a ``BooleanOperationManager``. manager.union(contours, pointPen) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Performs a union on all ``contours`` and draw it in the ``pointPen``. (this is a what a remove overlaps does) manager.difference(contours, clipContours, pointPen) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Knock out the ``clipContours`` from the ``contours`` and draw it in the ``pointPen``. manager.intersection(contours, clipContours, pointPen) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Draw only the overlaps from the ``contours`` with the ``clipContours``\ and draw it in the ``pointPen``. manager.xor(contours, clipContours, pointPen) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Draw only the parts that not overlaps from the ``contours`` with the ``clipContours``\ and draw it in the ``pointPen``. manager.getIntersections(contours) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Returning all intersection for the given contours BooleanGlyph ------------ A glyph like object with boolean powers. .. code:: py from booleanOperations.booleanGlyph import BooleanGlyph booleanGlyph = BooleanGlyph(sourceGlyph) BooleanGlyph(sourceGlyph) ~~~~~~~~~~~~~~~~~~~~~~~~~ Create a ``BooleanGlyph`` object from ``sourceGlyph``. This is a very shallow glyph object with basic support. booleanGlyph.union(other) ^^^^^^^^^^^^^^^^^^^^^^^^^ Perform a **union** with the ``other``. Other must be a glyph or ``BooleanGlyph`` object. .. code:: py result = BooleanGlyph(glyph).union(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) | BooleanGlyph(glyph2) booleanGlyph.difference(other) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Perform a **difference** with the ``other``. Other must be a glyph or ``BooleanGlyph`` object. .. code:: py result = BooleanGlyph(glyph).difference(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) % BooleanGlyph(glyph2) booleanGlyph.intersection(other) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Perform a **intersection** with the ``other``. Other must be a glyph or ``BooleanGlyph`` object. .. code:: py result = BooleanGlyph(glyph).intersection(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) & BooleanGlyph(glyph2) booleanGlyph.xor(other) ^^^^^^^^^^^^^^^^^^^^^^^ Perform a **xor** with the ``other``. Other must be a glyph or ``BooleanGlyph`` object. .. code:: py result = BooleanGlyph(glyph).xor(BooleanGlyph(glyph2)) result = BooleanGlyph(glyph) ^ BooleanGlyph(glyph2) booleanGlyph.removeOverlap() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Perform a **union** on it self. This will remove all overlapping contours and self intersecting contours. .. code:: py result = BooleanGlyph(glyph).removeOverlap() -------------- booleanGlyph.name ^^^^^^^^^^^^^^^^^ The **name** of the ``sourceGlyph``. booleanGlyph.unicodes ^^^^^^^^^^^^^^^^^^^^^ The **unicodes** of the ``sourceGlyph``. booleanGlyph.width ^^^^^^^^^^^^^^^^^^ The **width** of the ``sourceGlyph``. booleanGlyph.lib ^^^^^^^^^^^^^^^^ The **lib** of the ``sourceGlyph``. booleanGlyph.note ^^^^^^^^^^^^^^^^^ The **note** of the ``sourceGlyph``. booleanGlyph.contours ^^^^^^^^^^^^^^^^^^^^^ List the **contours** of the glyph. booleanGlyph.components ^^^^^^^^^^^^^^^^^^^^^^^ List the **components** of the glyph. booleanGlyph.anchors ^^^^^^^^^^^^^^^^^^^^ List the **anchors** of the glyph. .. |Build Status| image:: https://api.travis-ci.org/typemytype/booleanOperations.svg :target: https://travis-ci.org/typemytype/booleanOperations .. |PyPI| image:: https://img.shields.io/pypi/v/booleanOperations.svg :target: https://pypi.org/project/booleanOperations/ .. |Python Versions| image:: https://img.shields.io/badge/python-3.6,%203.7-blue.svg booleanOperations-0.9.0/appveyor.yml000066400000000000000000000016161356304034400176010ustar00rootroot00000000000000environment: matrix: - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7.0" PYTHON_ARCH: "64" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" install: # prepend newly installed Python to the PATH - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" # check that we have the expected version and architecture for Python - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # upgrade pip to avoid out-of-date warnings - "python -m pip install --disable-pip-version-check --upgrade pip" # install/upgrade setuptools and wheel to build packages - "pip install --upgrade setuptools wheel" # install tox to run test suite in a virtual environment - "pip install -U tox" build: false test_script: - "tox -e py" booleanOperations-0.9.0/pyproject.toml000066400000000000000000000001161356304034400201170ustar00rootroot00000000000000[build-system] requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"] booleanOperations-0.9.0/requirements.txt000066400000000000000000000003301356304034400204650ustar00rootroot00000000000000# fontTools.ufoLib is not imported directly by booleanOperations, but # the test suite needs defcon, which in turns requires fonttools installed # with [ufo] support fonttools[ufo]==4.0.2 pyclipper==1.1.0.post1 booleanOperations-0.9.0/setup.cfg000066400000000000000000000022571356304034400170340ustar00rootroot00000000000000[metadata] name = booleanOperations description = Boolean operations on paths. long_description = file: README.rst url = https://github.com/typemytype/booleanOperations author = Frederik Berlaen author_email = frederik@typemytype.com license = MIT license_file = LICENSE classifiers = Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Topic :: Multimedia :: Graphics :: Editors :: Vector-Based Topic :: Software Development :: Libraries :: Python Modules [options] package_dir = =Lib packages = find: python_requires = >=3.6 setup_requires = setuptools_scm wheel install_requires = pyclipper >= 1.1.0.post1 fonttools >= 4.0.2 [options.packages.find] where = Lib [bdist_wheel] universal = 0 [sdist] formats = zip [tool:pytest] addopts = # show extra test summary info -r a # run doctests in all .py modules --doctest-modules filterwarnings = ignore::DeprecationWarning:fontTools.misc.py23 booleanOperations-0.9.0/setup.py000066400000000000000000000001471356304034400167210ustar00rootroot00000000000000from setuptools import setup setup(use_scm_version={"write_to": "Lib/booleanOperations/_version.py"}) booleanOperations-0.9.0/tests/000077500000000000000000000000001356304034400163475ustar00rootroot00000000000000booleanOperations-0.9.0/tests/__init__.py000066400000000000000000000000001356304034400204460ustar00rootroot00000000000000booleanOperations-0.9.0/tests/testData/000077500000000000000000000000001356304034400201205ustar00rootroot00000000000000booleanOperations-0.9.0/tests/testData/generateGlyphDataRoboFont.py000066400000000000000000000015641356304034400255410ustar00rootroot00000000000000import booleanOperations try: from mojo.UI import getDefault, setDefault hasMojo = True except ImportError: hasMojo = False try: CurrentFont except NameError: class CurrentFont(dict): def save(self, path=None): pass f = CurrentFont() if hasMojo: glyphViewRoundValues = getDefault("glyphViewRoundValues") setDefault("glyphViewRoundValues", 0) for g in f: g.leftMargin = 0 g.rightMargin = 0 n = g.naked() d = g.getLayer("union") d.clear() d.appendGlyph(g) d.removeOverlap(round=0) if len(g) > 1: for method in "xor", "difference", "intersection": d = g.getLayer(method) d.clear() func = getattr(booleanOperations, method) func([n[0]], n[1:], d.getPointPen()) f.save() if hasMojo: setDefault("glyphViewRoundValues", glyphViewRoundValues)booleanOperations-0.9.0/tests/testData/test.ufo/000077500000000000000000000000001356304034400216675ustar00rootroot00000000000000booleanOperations-0.9.0/tests/testData/test.ufo/fontinfo.plist000066400000000000000000000014261356304034400245710ustar00rootroot00000000000000 ascender 750 capHeight 750 descender -250 guidelines postscriptBlueValues postscriptFamilyBlues postscriptFamilyOtherBlues postscriptOtherBlues postscriptStemSnapH postscriptStemSnapV unitsPerEm 1000 xHeight 500 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/000077500000000000000000000000001356304034400252665ustar00rootroot00000000000000booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/Q_.glif000066400000000000000000000041311356304034400264670ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/Q_T_ail_reversed.glif000066400000000000000000000041361356304034400313440ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/contents.plist000066400000000000000000000015371356304034400302060ustar00rootroot00000000000000 Q Q_.glif QTail_reversed Q_T_ail_reversed.glif ovalOval ovalO_val.glif ovalOval_reversed ovalO_val_reversed.glif ovalRect ovalR_ect.glif ovalRect_reversed ovalR_ect_reversed.glif rectOval rectO_val.glif rectOval_reversed rectO_val_reversed.glif rectRect rectR_ect.glif rectRect_reversed rectR_ect_reversed.glif booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/layerinfo.plist000066400000000000000000000005601356304034400303340ustar00rootroot00000000000000 color 0,1,1,0.7 lib com.typemytype.robofont.segmentType curve booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/ovalO_val.glif000066400000000000000000000012241356304034400300520ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/ovalO_val_reversed.glif000066400000000000000000000012351356304034400317530ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/ovalR_ect.glif000066400000000000000000000011661356304034400300530ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/ovalR_ect_reversed.glif000066400000000000000000000011771356304034400317540ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/rectO_val.glif000066400000000000000000000012101356304034400300410ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/rectO_val_reversed.glif000066400000000000000000000012211356304034400317420ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/rectR_ect.glif000066400000000000000000000006301356304034400300420ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.difference/rectR_ect_reversed.glif000066400000000000000000000006411356304034400317430ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/000077500000000000000000000000001356304034400257025ustar00rootroot00000000000000booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/Q_.glif000066400000000000000000000031001356304034400270760ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/Q_T_ail_reversed.glif000066400000000000000000000031251356304034400317550ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/contents.plist000066400000000000000000000015371356304034400306220ustar00rootroot00000000000000 Q Q_.glif QTail_reversed Q_T_ail_reversed.glif ovalOval ovalO_val.glif ovalOval_reversed ovalO_val_reversed.glif ovalRect ovalR_ect.glif ovalRect_reversed ovalR_ect_reversed.glif rectOval rectO_val.glif rectOval_reversed rectO_val_reversed.glif rectRect rectR_ect.glif rectRect_reversed rectR_ect_reversed.glif booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/layerinfo.plist000066400000000000000000000005631356304034400307530ustar00rootroot00000000000000 color 0,0.25,1,0.7 lib com.typemytype.robofont.segmentType curve booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/ovalO_val.glif000066400000000000000000000006371356304034400304750ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/ovalO_val_reversed.glif000066400000000000000000000006501356304034400323670ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/ovalR_ect.glif000066400000000000000000000006011356304034400304600ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/ovalR_ect_reversed.glif000066400000000000000000000006121356304034400323610ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/rectO_val.glif000066400000000000000000000006011356304034400304600ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/rectO_val_reversed.glif000066400000000000000000000006121356304034400323610ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/rectR_ect.glif000066400000000000000000000005111356304034400304540ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.intersection/rectR_ect_reversed.glif000066400000000000000000000005221356304034400323550ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/000077500000000000000000000000001356304034400243245ustar00rootroot00000000000000booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/Q_.glif000066400000000000000000000037001356304034400255260ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/Q_T_ail_reversed.glif000066400000000000000000000030771356304034400304050ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/contents.plist000066400000000000000000000024311356304034400272360ustar00rootroot00000000000000 Q Q_.glif QTail_reversed Q_T_ail_reversed.glif oval oval.glif ovalOval ovalO_val.glif ovalOval_reversed ovalO_val_reversed.glif ovalRect ovalR_ect.glif ovalRect_reversed ovalR_ect_reversed.glif oval_differentStartPoint oval_differentS_tartP_oint.glif rect rect.glif rectOval rectO_val.glif rectOval_reversed rectO_val_reversed.glif rectRect rectR_ect.glif rectRect_reversed rectR_ect_reversed.glif rect_differentStartPoint rect_differentS_tartP_oint.glif zeroArea zeroA_rea.glif zeroAreaSelfIntersecting zeroA_reaS_elfI_ntersecting.glif booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/layerinfo.plist000066400000000000000000000005621356304034400273740ustar00rootroot00000000000000 color 0.5,1,0,0.7 lib com.typemytype.robofont.segmentType curve booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/oval.glif000066400000000000000000000012371356304034400261330ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/ovalO_val.glif000066400000000000000000000016321356304034400271130ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/ovalO_val_reversed.glif000066400000000000000000000022761356304034400310170ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/ovalR_ect.glif000066400000000000000000000013141356304034400271040ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/ovalR_ect_reversed.glif000066400000000000000000000017221356304034400310060ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/oval_differentS_tartP_oint.glif000066400000000000000000000012631356304034400325060ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/rect.glif000066400000000000000000000005241356304034400261250ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/rectO_val.glif000066400000000000000000000013261356304034400271070ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/rectO_val_reversed.glif000066400000000000000000000017341356304034400310110ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/rectR_ect.glif000066400000000000000000000007561356304034400271110ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/rectR_ect_reversed.glif000066400000000000000000000012741356304034400310040ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/rect_differentS_tartP_oint.glif000066400000000000000000000005501356304034400325000ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/zeroA_rea.glif000066400000000000000000000002041356304034400270720ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.union/zeroA_reaS_elfI_ntersecting.glif000066400000000000000000000006561356304034400325740ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/000077500000000000000000000000001356304034400240045ustar00rootroot00000000000000booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/Q_.glif000066400000000000000000000046701356304034400252150ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/Q_T_ail_reversed.glif000066400000000000000000000046751356304034400300720ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/contents.plist000066400000000000000000000015371356304034400267240ustar00rootroot00000000000000 Q Q_.glif QTail_reversed Q_T_ail_reversed.glif ovalOval ovalO_val.glif ovalOval_reversed ovalO_val_reversed.glif ovalRect ovalR_ect.glif ovalRect_reversed ovalR_ect_reversed.glif rectOval rectO_val.glif rectOval_reversed rectO_val_reversed.glif rectRect rectR_ect.glif rectRect_reversed rectR_ect_reversed.glif booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/layerinfo.plist000066400000000000000000000005631356304034400270550ustar00rootroot00000000000000 color 0,1,0.25,0.7 lib com.typemytype.robofont.segmentType curve booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/ovalO_val.glif000066400000000000000000000022651356304034400265760ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/ovalO_val_reversed.glif000066400000000000000000000022761356304034400304770ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/ovalR_ect.glif000066400000000000000000000017111356304034400265650ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/ovalR_ect_reversed.glif000066400000000000000000000017221356304034400304660ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/rectO_val.glif000066400000000000000000000017231356304034400265700ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/rectO_val_reversed.glif000066400000000000000000000017341356304034400304710ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/rectR_ect.glif000066400000000000000000000012631356304034400265630ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs.xor/rectR_ect_reversed.glif000066400000000000000000000012741356304034400304640ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/000077500000000000000000000000001356304034400231755ustar00rootroot00000000000000booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/Q_.glif000066400000000000000000000034211356304034400243770ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/Q_T_ail_reversed.glif000066400000000000000000000034361356304034400272550ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/contents.plist000066400000000000000000000024311356304034400261070ustar00rootroot00000000000000 Q Q_.glif QTail_reversed Q_T_ail_reversed.glif oval oval.glif ovalOval ovalO_val.glif ovalOval_reversed ovalO_val_reversed.glif ovalRect ovalR_ect.glif ovalRect_reversed ovalR_ect_reversed.glif oval_differentStartPoint oval_differentS_tartP_oint.glif rect rect.glif rectOval rectO_val.glif rectOval_reversed rectO_val_reversed.glif rectRect rectR_ect.glif rectRect_reversed rectR_ect_reversed.glif rect_differentStartPoint rect_differentS_tartP_oint.glif zeroArea zeroA_rea.glif zeroAreaSelfIntersecting zeroA_reaS_elfI_ntersecting.glif booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/layerinfo.plist000066400000000000000000000005631356304034400262460ustar00rootroot00000000000000 color 1,0.75,0,0.7 lib com.typemytype.robofont.segmentType curve booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/oval.glif000066400000000000000000000020241356304034400247770ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/ovalO_val.glif000066400000000000000000000030521356304034400257620ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/ovalO_val_reversed.glif000066400000000000000000000030631356304034400276630ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/ovalR_ect.glif000066400000000000000000000015531356304034400257620ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/ovalR_ect_reversed.glif000066400000000000000000000023511356304034400276560ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/oval_differentS_tartP_oint.glif000066400000000000000000000020501356304034400313520ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/rect.glif000066400000000000000000000005241356304034400247760ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/rectO_val.glif000066400000000000000000000023521356304034400257600ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/rectO_val_reversed.glif000066400000000000000000000023631356304034400276610ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/rectR_ect.glif000066400000000000000000000010531356304034400257510ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/rectR_ect_reversed.glif000066400000000000000000000016511356304034400276540ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/rect_differentS_tartP_oint.glif000066400000000000000000000013351356304034400313530ustar00rootroot00000000000000 com.typemytype.robofont.Image.Brightness 0 com.typemytype.robofont.Image.Contrast 1 com.typemytype.robofont.Image.Saturation 1 com.typemytype.robofont.Image.Sharpness 0.4 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/zeroA_rea.glif000066400000000000000000000003751356304034400257540ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/glyphs/zeroA_reaS_elfI_ntersecting.glif000066400000000000000000000005501356304034400314360ustar00rootroot00000000000000 booleanOperations-0.9.0/tests/testData/test.ufo/layercontents.plist000066400000000000000000000012271356304034400256400ustar00rootroot00000000000000 foreground glyphs union glyphs.union xor glyphs.xor difference glyphs.difference intersection glyphs.intersection booleanOperations-0.9.0/tests/testData/test.ufo/lib.plist000066400000000000000000000015631356304034400235170ustar00rootroot00000000000000 com.typemytype.robofont.segmentType curve public.glyphOrder oval rect rectRect ovalRect rectOval ovalOval Q QTail_reversed ovalOval_reversed ovalRect_reversed rectOval_reversed rectRect_reversed oval_differentStartPoint rect_differentStartPoint zeroAreaSelfIntersecting booleanOperations-0.9.0/tests/testData/test.ufo/metainfo.plist000066400000000000000000000004761356304034400245550ustar00rootroot00000000000000 creator com.github.fonttools.ufoLib formatVersion 3 booleanOperations-0.9.0/tests/testData/visualTest.pdf000066400000000000000000004276661356304034400230030ustar00rootroot00000000000000%PDF-1.3 % 4 0 obj << /Length 5 0 R /Filter /FlateDecode >> stream x[M ϯe|HA~`:NŮ7A~~MVz{l aERene4~߷Wrex<6ϴy;m6//_w?=^ۏǗzSmN8ejqˈw_w c_G|^@Os o?L|#H~4_ɗ48~O}4 )zEon݂~a,y6Y|,;$;+eS~xOKPJ<5>ms"HS#Hۓ 8 =YC[(M{Modsz[i=AJy_aYl +k5g22 B* >l*@= 04$#E\CWm%ڬyR5 mmm^FLWhXUMIuwzvGTM )S m|@AHYUؚX)ImEM@[=.Ny0u E"5wooV:9"a&`d-F==Ց~P+E{Z3hБ-S|fj5 VOoU-Bmq?T\^YOt h:F_z u{e>aV:5T[a]_6UAMUp]Za"p9UIw+j?\{0)A!ԋ;>n%u=؝ŲM{AyZ sNFǍ^by_ukm$UާLmLa5d[zi-2ֶpJ2}(zjvsaPaYȰZ %w{Y$Pf?B {5YH,Bц_ m)!R/Ѷ{SaC#3o 60k$KKԤub+=H,==C wheWP"[b!>IJ/SSBq2R [H">E/hCbʊ~;ZYqԯG+)oH=U z ʊ_VsVjR$~ΐP/G+iÖ3p#=Xu/j㳳xN1f+8>TEr-HE/.Ivf{ӖT4leCOc1jI$UaK.v}=ݮ5Y0 UT[x &v9 McKn=6zJ>a[~vrz@ʜ~ P:ȚFR/pƍ5!ӕ2.UE yqT Ea tt&? ްLlz@3dw&ΣХ&@y䎩 P&q)JZ60ޤ$cyL,c k'}dd?R/ 20H,l{/X'}dEƆˢdÔYY {xWoxyw=\zb^J^I{NE #PzYz&z̋~ڙ{S;=`;$Űu'}ŠJWKMoJbd"+kż񩃙F. jĀ h<(`^E0/I{1}Ah’>#H*f*ƺ"$nH*.~EMe%6#xHRV nA mȘE/ʴ,իbz댂[ܤWঢTVR2f%EOEV$AzQ.yOΐzp#-ypԔ F6C.kMTS<(>#tƟO'H$[%oЂ[4$S,~]I6B`{z-S$AZE qK(}X ]XV$aIzJi+=H:Hڳs ږdv=Q%uEԖuپ'UUQx!I|V^ZvӜ>Ù~bR&}".;l|H:_*"5<o V" pb ĥD4=,Ӄg|+"N"H~踇ebf8q>K{ D*JK[T",`~PsU(i 4Gđ"Kn=%I->P\S{-Ծe R.j_$A3!Ì[Ojڧ\cSj_6x+Æ~2֟ ǗVr|ϭ X69̻cv-?>4&?`&> endobj 6 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 10 0 obj << /Type /ExtGState /ca 0.1 >> endobj 11 0 obj << /Type /ExtGState /ca 1 >> endobj 12 0 obj << /Length 13 0 R /N 3 /Alternate /DeviceRGB /Filter /FlateDecode >> stream xU[U9 -Ct)Kݥ[kAd$L }*⋫IA-zRPVw"(>xA(E;d&Yje|oB%6sɨc:!Q,V=~B+[?O0W'lWo,rK%V%DjݴOM$65GŠ9 ,Bxx|/vPOTE"kJC{Gy77PہuȪu R,^Q 9G5L߮cD|x7pdYiSශX]SzI;߮oHR4;Y =rJEO^9՜gT%& r=)%[X3".b8zᇅJ>qn^\;O*fJbﵺ(r FNԎXɂHg ˍyO+-bUɠMR(GIZ'ir0w]̈́Ϣ*xšu]Be]w*BQ*؊S㧝ˍaa,Ϧ ))4;`g>w{|n Jˈjm*`Y,6<M=Ұ*&:z^=Xźp}(oZjeqRN֧z]U%tACͼ^Nm{Х%cycE[:3W? .-}*}%>."].J_KJK_͐{$2s%խטX9*oQyU)<%]lw͛or(usXY\O8͋7Xib : mשKoi1]D0 N }` **6?!'OZb+{'>}\IRu1Y-n6yqwS#smW<~h_x}qD+7w{BmͶ?#J{8(_?Z7xhV[|U endstream endobj 13 0 obj 1079 endobj 7 0 obj [ /ICCBased 12 0 R ] endobj 15 0 obj << /Length 16 0 R /Filter /FlateDecode >> stream x}VM8 WPdɖ}X`oؓwiL؟H*[`ɤ'/ρ3;B=ř}ZwQFߥFfaYޞ=-U4X#b(`t,Ge@/aTs==i txptS}7|71M1xˆ5zF\Of\&gȶJDqҚaPEpHn2'<٫`% Diq/w>bP7hd1]#keN 6NT$q2hH10~koE{D .fL [gq6V.͂8j,doaPj4MSc-nQB 3mg!t1M>>ݯo{*~l׽?Oy1a,cm>ʳMCY+ -^{ u[$/PzgsWB|s|M\6@jЂ4~q]A85UinȻP4Bt^P#wWoڽ#;#pAO3O3̼L]$iN/Qn endstream endobj 16 0 obj 957 endobj 14 0 obj << /Type /Page /Parent 3 0 R /Resources 17 0 R /Contents 15 0 R /MediaBox [0 0 1910 498] >> endobj 17 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 19 0 obj << /Length 20 0 R /Filter /FlateDecode >> stream xZ˒+AC<3W1>^9v(G]Ym֢ Yzu_~}{h˱~ğǧ~imz?<=?~{NS.ᙒ^W=}7z`W7"yx k1ຶpV_EsǻFn5~~P5mbQy[UC?ɩ:m~u\anN߉&.&, DJ 1M Z銭K"'梛xLMK\HZ`0uIp%z(򉡊%5ِ,0YE\PTA Lzw: $d7"k U-q rn:KVAzU ;ToXomк%^Pyl]%+=VbKD/>u\O3(2)cx[A9% @gj9H+)N*椛IhEbD%I芭K"E$I$nnk&Dյ)YEB.7K"ϛ'%NJ` [UDRП:K,&Y$mL67xEYMaX':.i^.fDnq/.&: 90Lꪎn0Za`7A湺jC!t9 K([:uu\WF׬@Ki O[ ;%Q4 ,Q.K"۲7Bԣd=:ęAd@7qx>b I^j{RPyLr.Y$ ,Iu2 %ކ)w5"CL $Rb$,8Fk } =`9K%D`.zb:1P1DYz J 3 ?鄴d!Kzb^$>':E;S٢$sZG& ::TͻLepy wu%e_)[GiMU\IYXZn=-ˁ@-׼'X8{['F88K:9%_e$F>YϞӺ%suV+#^ӿ FOk%@Z"$ܟ3E\LKv} 5yY,aDV٧E%q_ s0v B',(Gh'"O/t }zB',`) N'[W%q!9/.'UBH,!*C TʜN O.*䢳z{Q4,#`0N"Nbb(O@1G8ɬ LY& ZkY>IJPjkYRBr(!)kK)I9k\/%)GnV=gZ¬=Rp'm.8$=q!TyάӰZqU.{S.XhYAJ5ġ >$ O*ꩬ %z xG|!R"G|@³; +n//yնEFkMcrRl:>{L- O2 ,aY1xvA:8&ב'tS3frts⩅O-ֳs),ޞ, I m;b83)Z)Ĕ꨻-P7Fvl0@ܠHS83lAD7ę^zdaC\[tjД #g [KEۑduI4Laӈ/d8d m> endobj 21 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 23 0 obj << /Length 24 0 R /Filter /FlateDecode >> stream x]ˎVr+zѥ$ڎ`n^55h$|79Gv Cʎbq"eǟoe~/_@i/}//:~z re<~.O?8_>凷_?6~?ᦺշ{/}|}~?w/Ao#.q} ?Z&*ſ?;s4O_Pߝh߾~PO+0c. 5>S/.>W} E??E'!4nwy xL࿼r۲A|x=кB棞O]5H֞a}|Űn]>v}'*IXiYgZW8vZHXq`Yh~RjtH UNkO kZt.0tvwB3uo᭿.G{iM)7CFKY4OHU[/~-M۝ÿBB䃿e!(VNF NʗsY ZYOnH\|>]>u~~"+r)p"E (% ?!'e =o}}[$nJj0/,$܋HOն=LD-,{ {%˘dHM=F P z-I}, ]T WX!U(YjO@)RZϷ(|Y"ЪbϑЉzr}a#Y lsSs?)A1:#.m*!gZ |+@%PYB)@-5=wp 5`m=nmcm3i6!(6m-oO6Y-+F2_6Rmʯ۔^mJz̢T.Wm@&=\I郢2_OR#71_O=:gJVzꏄ|=eSc(!IbbvtQ oz4AoG__M ꄷDC.z:i-`-ƙ~a3+4*L+)]bL#S` 5Az>p2a ŎY# -a#\Ђa:(lH蕏t > ϖtgyi $c\m/ee_9^a*k3 ˆ3YDQwXUBV\RY ǘ(]| –g>5X%[y.S"BkX3@ o,<֒@L\@}X8@ Px gsY'/馤n'"H~fIPK"i7(`V9EIS:@(k>"\^8RFp)Bl] m z`ߠXlV!U+jq=6ކL,ې6ƞƮ^ꞑzp(P)1I$sgocH 3ROomUoS }[Mj=#e6AQ.S9ZvBʾq0ކBAEڒa51E11,HAh|XO@ayySrxI $ ;SAV!dg!wk@to=͇/i3wLT'U>8^G&Py2E-EShncK ARm(Ы P@:"|^T.On'5Y( ܧP=]NW+"!0 oni)3-@n&[̼\JOҹ8k@giLl9jjt%2H5m!Wʻ-` آ񯤛§B4fE\5尾,3Hl!{~ȡH+6v.uyܰac? vx?qsw^PoL0cۋks7a|m% {R)dvkXDF6ivr?0*Ho;ڊm(b_ԍnͥ z;S;G9!lSˆ?ZP|~d"m|iOӨ :,R5+ƈ #Đ ˆ*$kCAO}jOb٠ex; C.`JS|zbHm`>d@E!v6_)D rUbᵰ51ښ/ݮJHU)Ô-J?)}0E(QOOҹrkzSU[W)`O5z$FD%erMtQa{Bݤ5u/ȇUlAjK*8BE[tI[/T&rBI(Ptl`eXWJ ii]z3NS6WQpek8"fM;|`JFeP6'hˏ ѩ/m" HOeA^DI|΁zZ6gKU` v%*0B]dSTTȇH8؟}4 æ/86#vCDa}B4fi{O@"{C0 "AtJa}gHR|=K wiدmO} 5V$|`pa<,7VQ71Eej#|VBB\( '=/kOr{NV]7ɺy{!jaG GEpZeۏ9xJ7jս5x(W^lvA۴*'v*}4()K[qA +z8 v"<+cP=1Ս@i>myO3=tqJ8%s1iFHWaPsa!'Rj1Yl8A m ڻ:}#!$D"Ŷ iF؏G > 7|ޕ aCi LٯNp>7fghu~`*;Z_a'oeSPV~?(/Wl|v ]A5m+lb[Mz;l]Rc-~ x Mq|@ Sn8t7cQ}Kz_Z` gޞ)R9V.ϵ؇b-Э'g11Is`xk`~}֞iQ<d9 dr+/퍲F`S-8!ZB[ϸ#й/k@XO $&D~r }>rZ?rs#/H 6Į_[ÿ"6{)3;GրYi>4'~(e{c&Y#mҡ_ݰjd/N_%rWD1),8`UDU|NS>g w_4Qs sœ䃯a/˖)9w,f |'>0O5}N$y8;X I*\<.SfbTj9)!mI N0|jBZg>IA@% 9)`>FPT쯭),yɇX@(WpFsӈܠQMk*˴ؓBYd0Ƃ/BTi"imd7|QNkAR 6*4J)$.Km֚S() <*5d)DCL I@ՃF}OU~QSFOh,)55{F]u) ŠQ-#5! :E9 uB"ub ~I)\kt<5(Ȝ7mB4>HyhYK{|Yr3:I`D<0D!b"tbk`kOiyhsY^)Z0mN tQ$D4B_OӈgAL[x袤I 4"ь=W0yŞ ɒJ1O()H1(|4I7 8b0@=JK?׷=I wi&R|@GRE*!M#N>oKOqxR92Pqz_3N Zuc|4_8RO3} N=qJjJGX=NgҁsqA8ia\0 Ôv)q')F+;QtU—S"]=)qgH}YOzpf=ݫ~ rv>xkouR]/{?5@Cp8IBEdmk {k?J~?C؉eas X'GuKozϊ;tZ8]׎)mBW%[q2JZƜ!nZ[(Q7~5 6ˋm,&cs8u RGgE>Eڠ7 v5 dЧۧ q#NշI:ONkv|'YcFSH9@Di){Dj=GDHI|F"0@Y[ >֚OQ-QQk?E@@|ҳQNk FIT(**hza[2͖FH@5etm.سtQ`_ӻ[ ,-ܥ6mڂ4;aU% ;Xa.mF;;\'el^1ӏl.|o~_~C8rmru>}SPi'T_&~sk`˚q!3_Bˮ7~4敛_] ;&cR泓18݀_abJ:$zb"t)k`n}Haz㳊oBHt}<"oF y!s5 %g|+S@A ,x:$)+Ò.`! H9Y_WK6w4+A !tB-? i)>_yMWs<.G|{3bO 2E6؜c}g0I:܌سݥS--܅i/!"Anm.u&e^th޻#ķ2&6we{1=2x9yd{a 7@|r9|INV]a~1B4f|[(/;"CZj+R(ʹ4МP]L߀Ҿcת j?y{< ڨ|V"ת{H P2fg'+kkzDuw endstream endobj 24 0 obj 7605 endobj 22 0 obj << /Type /Page /Parent 3 0 R /Resources 25 0 R /Contents 23 0 R /MediaBox [0 0 870 290] >> endobj 25 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 27 0 obj << /Length 28 0 R /Filter /FlateDecode >> stream x]M$mׯF ?u}tu @6YRcƆ,'<>F7Yk5ټ,~U=?ߎ?_4<?48 >'Ï2Mut?_8 _je?|^L7R/_P;~o>}58~;H Mq?U:s_owwۈf.~4x~B!mƧ?)~_ЍwBo.ֺ4?ń!^W8Cߤo dIJ{M hMwןO;8@ָ0<쌝4¹RO#r/m/q^/9m=_VP@|s{OsKL棞fi<)c٧AnI ⣞yv>^>(s] <#8[5oC>pmNڜEf |i}(oBpBYS:wg m}Z0(*[րg=Wa3iIOKڇJA|8oMsPLsڇґb 䓞[ҀAz>mȃ7͏c…y>ˊ- dɇmBi^Ӕ3m^zED>ϓ;HSg,ҞmT"kF^b,;x'^ e02\#;rzr}`,.Nú0X2|ᤊ^)-}E' LS hSH t 5csr0+r)Zcsf0RdL؇|a efw_JW ҳEJEԿsh"mfv-6+Q=5Y=p>^Gt*M%7huB=yImbv-*SvcZ!(o7JYԳj[+&*-s̤ k0eJLٳ*Sޒ9V`.ݕ EL3%+ʬ! L9{Kr~t3]*!<^EW:3TkI8|+CgN.˜i3|9gJ\3t)Ҁ#=20g5H |+S :Rbٍ!d _ņ->MKPbja+x%Ԏ&˔LD` >JgHu}T a~lʿngzWͤ?-רoEac YiMRFi`>FEi*m`0Mּ:usP4p@mM%B]hR^ 'ΦIVٛuYJۨ!t5toc&4 wlxgfzC(1-[()200XSk$i;+}d@`2MJv RʨLi){1)|W|^J'Zh+@v OQHH=}Wv-xN}w32~J/ ;h~&fz==ɢ0aDv1Ѿ#vLJ`0_ɐ2a Dײũ[#|5xPKL֒D4rc$7l t9<դgq5`0Ev&i ݃9؏єni$]mP3B}`Y\-.Vt5j` Z[H=Aߐ( 3c}9t0Y|̖EǶ' E0/DY:+̙2|=_#1Z]50 }*TM>,,o8yaSzO|m$=_!+ ') M>^<ԽVC:M@ Sx‡m̉FTqhrT[gҋ2cQtŔԠQr!t)-uQ5<ח~m(/M#rhQ(Pt\@zV`anOB% fB;V'IJi]Qh/G`,XS6H U62H[Ae qfX/_X6qͱfmJBP FۀEHxZ+n¶)n<)xOhǗtnzF twk` $_+ Sg;zk|$63g.  =3AR KXvЍD=!) Laf2_ BR#QMC,XN:-/^-pf쀽N~ !7˚wXRe/=0j kIsSk9V|ͷ=5&u%2j_y{QR4_j5{'!mJ=x&ǚV "]*v;SYX?tYZ Cc }.ۗKsB|jHAJzk'(2qJ8ϣ?kg r7XO\[gH=4!|Z(,8TeNL׸5=Σ>y\^pfza>}|:P}XE^Ҟq]:'e4B ih@tqiz- ŠҠeI ZP4 4Qg@iDXI@EZF'=,]MmSl2, `K%SeB `K,B 'h%e Ҥg[fQ`}#t 0ə:Qh-N*g  `je|-T]. %ZqV0)pO e׼%B1C}:||iOSt/hE p|kҞK[Tu!A;BÙU U/TA=>LaF7_w8^mTi7B-F嬖IihfTSBa@ڨ']4^ ;q|O:- wO>^R:3_^@P2]Pk&Y/Z,+V SPp=hL5 Բ1/NNJϠ^_V]Z=rc\[aJKgm̀E^۬=u5 2NPB- jECjV. ,*ܠS{<_NM; ;=04_GGL4[_6 xx{ڼӷsF~CFhMVβ h7c0Čm`RUiaN6f.} <n_NA0o3ۡi'/@ԝ;GgzV9WV-4X5|mzI0 D`%tOirµ FmPO/2ʙ6S%p83GAlԫsK/OբZb X)T/'/|a $Z -аl1Wzͭ헺{E;*)z[f+n?5cJ|~#Dp7S+~'[c3 {'KJ?BLOm LF.ji.L@]`UZDM`K7S 0 %&.8{,K]ki|J{k ~]*VG4$˳  i{o XQ&!cz@ t֣x\6Q <.u!qkJ:ܰvwE y\p`W:B}Qp⣋O^@x_zDt߆p̬>t8CgK|{Iiy˰W []PK{@a`]5ڵ'nO -S飛Gi {WA&CCT"BIen}Z X߅ײ@K %z,pk=yf̋Sao5[Oe 6č̦B,|n[k*YWsjHUc_+@YhC #~PPKwh P+.jE{ )ZK=#K:50x.j.^gi`@9YxrBX(4 \tgr3րf 0 FOE,F(Zʘ6T|/4 #bykeyW ua E53a.Wu/&')UHO+Wz|,ƊXc9c>WK#CV=ST.r?V܏Eͣ)V]_9.rƊE? 8)E) wPթ,%zR|)j{a>m?֫'p_k|n/|_-f  |uC)lgB_wl~7=ދwq'ޓw3hݝiNj=Rs~}{7 ;}<ڕ&iʼn]=Y䝵~m}R7 OگM|mm'&v4U7w +&9Dn|?SQC5'e*&#8iW47km=M!3_o,wnk}Sp~|]7I'`·9LCLL{u+HBsvB\J9y=MBsJJZҹַ\[O~/3, JY iK#4QYck]⧁4&_c9=%5n=x:1/Yu;i/jGs{;Ou8eiL +Ʒ)Wڤi,(qu/ZP'>/k}kU3\g!n8oyr+A8 D0$YyQ=㴈/%}{\+'*=)v.x=ga?kҶ08cm;Ma@>#pN9#POG-Bl $ڼL:\ -pj ~i/T(ܲF~q:r_324"Ϟdr(|~z}[q\z/yݎk}Sŗs V" 4.Zz\B3q K}i>8܉zptHd0k}RP.k}l- Ľ\|Z Gh}{ko N0֧Ƈ9B ZBk;a L*nŗP H~Zw{A;}YcZ`P(gdVրkv֑ok~í %) ga"!Ԃ#L!2_)Vˁ:u֡&K[@7g)L9Y'H)ʨyrk Fy-Iйx,=Å~`_?k|Z0}E?U W\ÇW>n/ _ Wѫ_(w!]|w؁RP֧osq{?t:xWcX$R B&V>rP>f {L!2#0TVG<`.JGp@ZM3t/k@e=͇ ũdX-߹xK+Sx_d샛4_L@p5^}ދ p~>%zy\Z h{x=Zx^o?h5&kaI'޲·"k0QJb p)ޭIlFBh R/FEz(YYKFA /dzd=]çM8Qbѣ#Bɦ!>r<<8$e|hNU8Q׆=L-%$q.ѱsl L.Y{J [zYԣ\06{ȧK7kÔ _Zk?}K-4_ҞTB$F]4.]H|&E9,n8fXL wW endstream endobj 28 0 obj 7572 endobj 26 0 obj << /Type /Page /Parent 3 0 R /Resources 29 0 R /Contents 27 0 R /MediaBox [0 0 870 290] >> endobj 29 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 31 0 obj << /Length 32 0 R /Filter /FlateDecode >> stream x}M$ɑ=E6sˬK,E ط\ |WeVϐf@6__yG{D~w7q=} Dk\Oiſ/_O|%8?}z>-~ <w/??3>Y ӽ>}~_;pǗ/B'~xqٖrF߿Cagi|ſt儷ŋx'~?A_|IX/5pۯ;~`ab?~ƫ?L-Cz ,I nЌx=^w)m՗[4 /~']p𤷇'ݏϧ?d0T sy~i]o.?]d.}mt}>m^ pk.oa#.;$:>]Qj!֣<^rd;W,Lj}~(sϗX?.eH@BM*b1\\z&$f+D=,lIlmj}hSOŅDlؘbEho o x[z3>G}\xظě%ěNvZ/~+^oD$zzÖH&;WH+D="$ w*Thr|FH֩ӁqǺ}?ۧ %?Z+ܼQ1"^?>-W`%[} ܣ޶-ZnI[Xy>,Dvr~nl=G wp+xzOrKe#ȅeS_˂\dSqKERCҡz0z)IV7C QkGXU[f>9_ZOި隱>o`-+ R5u F*@=+QEX}HOzB (x_qgi@ g 1mcV>v}Ѷљm2$6=GhXBh1֒6wjRhh{E'.K6MqtMmsMBxhz!{}$%_RwkT{p]Z3O"2$1J%ײ3hQ7ѐ<ϯ|SIAڜ@KHɺ"W:wdu'^ú8mvઙ&7*ܨxLvMx1IH6Bz[XB j CFm \l?bYWO` o[z0ymCI@z\m#GHvW^^H";8(MOc KՉsxe]z#֔g ˖tB>I=fC.%@VqORX%4XˣYc}ϐ-@=K/)fғ#HIIb}dFkLoEc/Ifr.R“Sbr>DA9%mډ$R1$IvhE n*QC,Rؔ塒K!)5eHz`1 ~V*Kʂ\9:jJ[`I1Գx/vY^) (Bivh* ÜT_AAvH"k&b(p"o|@rFmQ.6ƜhcVG3m3DAYP3rMa9M1Ci6h [-ʜOraڜ4 2SЊ Q獋UeD6l ^YK;isQ1m/LElzOxiPqyyKxdIff ЂB4ϠG_mٽF ̠R9+2M_}ĪE _ -w:$f@쳤!2{q[>XPjow[,k;=@bzja&1dgйPr=InnW,\$ =XFK" N|WB& ,/և{4Ъt'}?/wdbD%*F zi!l1&C#zFE tCpOl%">[%K_m=>"v㊖n6I Ԭ[ Sπ^GZBa֣8% ,/Y^XZK0t.#VzvۯK7 [I)L5ӹKcsYG@0Zp*UroFkOscts/{^Zۍ,S+x`K ŦjRR߾W:T ZQT96cm[-wh̳7cN1RHNgqX%a`HWIڛ1@APAwpv\ SHcSs]1?mE!iQ/pJtmMv^?c=v{F;rq@@wxw]~4I)VCH =#8r?ҿ o3z}V [FЋ,mAˢF :gUʑd^ztO&+1,yːt}|r=:]-sKN% G>q +g^=pxg«'܁&m O1lqlܪFC[zzl=-ky17nU[7G{/"P$^7ͭ#^/nU/J^=:m^m^&^3S[XWvc9b}MtOܰX})뮤RiSIO%C)$jGMIXb$B&M_&X_@4`G i:P&"mk1XXvR!U,b}l=G# }e3c/ӫP1xO=e.I[&A cx8ZE$'8]s^(_bXCnz(u\fʼn%1+I7 k|vX$xOA?s1:- c~zWF"uEntd=amsWİ qDzj>$>N4>bXav'־>n\ 1e-nCRN"󘨧-# ԃ^* 2COZRUPjJiSe'&>ISP[C~\yhKCtUx4adP*#~/$}]ūs,0~֭|4$II5 DG ƴ98WxG9mۗ~U{0V )r<>BqP%\Xy<^5+jPɏÔ[POw b]obj>~*א(ۚ= n`x0uJw[TAm~ңK" Z6s a{*k4rnGlj fg؎fMW4`rЪ?>PM6-|yjtIdK3!GF ~N;1"wqvSo,`c8q^>(Aq'.05>;">aQPV9Dr Ko`N am@aZzs:9LP!́9t.>zB Dsܐ@7dEU0p[m!~e %Qqȃnx-Uӥ >CQ5$׹Dp8;kI[NNb;jt%~d=G -ceH;UUo ޛˇH6!jWʐQ,? 3JN[0ܕ#up,:љeH*=ԣutFvZ`'}+yHׇv(?i`:ܨ@CE^z(D !6RLI-#@bwx^(@9zҪh{K:Zi#ps^;U N6Vi#H'vENJz!qz38z'-h ryt Nv&7 ]%"K4.]Z@=x1f.O,^)Mwt:p`""F&V2A:6{Q!?^SC$"V4ITk3QEc>AjϚ2ytR-[/K_:9t|31*681V{X Xv~ QA;=X J(+ug޺'|t^+ 9Q++j.Qc {HB/pʎU["  Fkնzo#USi2TA-_U^yv%ғYQ9V鑅͡I\w!;<ݎSbj*- >2gRd>3{Ѿg.gܝUA8̟ ;r_t$n,q*yG^&k/1 +ſ;*HbK6[tҬg +<僀שGCaK2E(KO-+8og"=|ꁻmW=\?`ZKX1I[Mb <|f;飯u{(ث׫ô@5GXHQizE,{@nRQOv^/ >_zq MW-y>!n-/-(qHOrU&7[D—ZDCd8iĹEd XɰwKq72x!NU!!3Sx Nu abH%Y'#qs NU a#n IZ]QkH^rيE|]wpܗ){E׷}'WdI[0pљ>Ky"Ԍ`l## wT%G/#,FNkƢj8^iH.УK(A]"jt߳N끿zs-TFPl!ɒp1n:,WB|Bg:~kJ34Y 4M# ixa&^,ـpD 4n7$;?~0[#G-΢?)X0gXEk _4.'g_G4] ^c[Dt대%CTktI.ٶ\Ko68:*=-y~ G 0 _evQ/4А א^}Q BrO I %=Ne 4|{/bүM޺J o57b Уt€.*zE.]6.ҋY E@M ڏ[.=l^ -K݃PSeQFh5OZ4 TvV>8",^l6/~|PXb3ǏMx,Jzr;o,h4zK痾gG=}W^87 : &2{8[G;$˭iG8R{v8\Uȩ$%=ى&m%< ýWVouK%VH۫?BDVmUmKr}77/%nihڪ`nik4[}5ob*H{A#+e"oòDg ϟz0ÞgBHLJ ,LSm_j8eo-de+#%MΩdzҸoh`Hcw0N$}dg_FDrDRR}hţ9C_L׏ƏX5 }4zd]Y@^/;=yjقzW7}ý׼>=܍fJӺ4{M[Bd=s3U'ȱȋ!#mpOrb,YNjt2*[@e;> 8d왎%vk8&-H.wc]CwF+_^ВRtvzL㌁l )x;x n,j!L{HX3V9z[@^/;yˍD^җ^!~.1'0'Mbg2 }yے\!=0ƜMs"OA-.̩/}C%;?.i*8%bf "|3Sݴ7\ g GTwhg֤Bẅ}w`>cc:4wypgH:xLVQ6Q\՝_wD՜4X3~G1~1waOԎ%>P3S3lJ1d`0 N4t`I[|~IDiwjg݄IY7D_zHkv|~i=%TWe{mIH}=B(¯ֿ$*SpE;8oN|(/ (폡7nHJJGc8sxIUc;PFoO:>we]Uu[!qUd Z:[u|}s-c;qlY [\CcHj]ރqZ€ovkh hUG8:%-=Ae$D!i;=se#{>V^ָR?,!߷^vx$ex+ R?,8H妲s䘼ꁌ/NLnc9җ^^{{b` A~Ў.u"(ғ#4!`)iz#x{sޣ/=jwY}*t?jр[xKvv5!DVz&V L=$ ~užF_o}ktVa9=/Ρ}w0~ௗ?W>![ykk"9Y@sH?ofƄ%dh|1fg!v^吶9~~8q5E>0hEYFR1TDR2Fe,ibShy3:})q-z>kG<ƨ&B;=X˽1^{Ɏz㈕P\)ሿڀR^`dH rE_8#:1WX2\ʏ_k ВzxtvbU0z>vb#;.dz0sS+'{WE 4E~ܡܕY 8*EIGf /ћ3*Z@m;]y0HP2v%0<5J"P,N[ܰN7v*"xb ;1֋eubnCJ ׸ n\ģxZ`=Ac xvZp; *z+nꭒ*0+ߨ'K,qN -RqS W8]ӮZtK[vo`2hP Vx'F&g%gY{ Lk"A}ˮÕ ub"c"Fg'fHzwYϝk0z@ucı^|Eđ!,Ėw崀i=4f}ZٝH` uwb|Չ+SEY%G'%2%1G'ƣ7ǟR ҡlo;!fAbC#TDI6vT PDe@+9C/;I|;%.#["Ĉ^轾_%>KdޣD=߁Ԯ^7*dvߤ-]}pĤSubW]\;1NǎJP%i]CzU_5$@vOfЉK]vvbF'&e:1p[B:1^{XBs!aRC q! W!ǝ9䐐YHvN1!˭+9!bɈ8@I,~=%3EH"z@"XBjO=3KG ШNEwbP޷G_)*h핒{b;}c ?/K9ۧs͛{^#_!NrGz0xH:|5xx EUq6'>?DYU$YKӸh4rtca$U KXwsɽv,V{pDi[QJFu֎R:ޥG;lvz~{%W5|wpU}pH&þ6@q $+euK(^Æ&C^pJ;C-`~-ò[ued<(ˆ2#vT0QfF(7ɏ7gpÆZu( >>?yI#xTNC׉-F m =mՊnqLڍo]oxcqфwDfޫN :Fz޻f+1`}-d {ǻjt|*'XMΪ-`evZ/jSzH6"fF:.NS'}\WTv(zOQrMW-U(in=!TUK9,UvǵV*. |J p_f 6n\j- !>\ endstream endobj 32 0 obj 11287 endobj 30 0 obj << /Type /Page /Parent 3 0 R /Resources 33 0 R /Contents 31 0 R /MediaBox [0 0 870 290] >> endobj 33 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 35 0 obj << /Length 36 0 R /Filter /FlateDecode >> stream xKdm+rvJ_3#;ëˆ,0?sz[R*_2` yp_??>v?~r?s=?=.㯾t8> ]Oϣ~Kt];-x_#П%lpxz>~Mo(ƿrIW?_6t_>M*r *J@ t'MKEw''{~oF9蛈X'x2dK\pW>QT>_9E78:Ii|XJ\Ko0SL ;/-,kaϲ%MYٸۧvàΟMʸ<>DedOo퉬{2K~.6pt~ϜnwNZ'9zS\\΃~/G}%ASCh_PޞN%sCOt dFEJʡ8QI?'shG44ӏP^Rآc_ XQ$iKbVMƷ9_nsJZP w%b\?h[#i~W67(a/gZ=.f?H6gN<>ΗF|)'|5f UJپ3(E`BVdn|SWj,3 &%ͪa|Me}j@~;1Vy𝮾*)irb=::%hz~Rfhpߓ>~VtSӍu9Ko*mr0R-W觜3%$HC)~m$-An$-g|5&u{Ji>"(߮Hȣv.p}ݮ l#,zڷ++f<(3 }iQRs\[R-FQ{m{k8|imr8E$ ߃BNڣQVH[o-)-Sm>;)BwXF Nخ!k {E66`| jNGoJJp _rZ|N%ZTYz:(:ҧwkOK^RB`@Br'e 7,)| <0S^x+1`Sk%%%P=Xw@vW=Wn 8Zƾ˭,7Z:-AsTc3mJb7WQe`-54_g|ڜb+|L}}2˺ؔ<-{;2G, io-AP8ջŧYРi )hֵNr| $%A@z,SU J'l=Ie= P'p_^|\a GoJJlfQP«>ET 뉮} ey`KP r@C |3(#,rz?(+Z>(.aĪ5z"(+}lGC:cRa5_QCrn uHfۏ#Otk|' ,q}nioh]۠qvڸ>w70@8AOװSn}io 89g9zZש$aYv oްu3p}nhRQ|nNM7冖vʐ/ѣ0R5˺JlXhnoy *13~.aNbI9hOK8ߣ7e SN~nJ@C'c|~ܿ[`K|Ҋ`wp;|OmsZ gaWgxiU.h ].Wc-h>:ψr{5:2$ ukP߮tiZt)i-2lͭ!ooI9^:%h7rb>Tyos˙78׳nZg^Wܒ7!?_^gZ5렸>9^ӑ{Q1(ao';M뺦|io%簷klH}Rڲ߰ہLzv^Xy=IYDSL=e7,%E%\FOǴsj=Yۘ[8Bh#=>qvPv"<;?g$~N-ePRljDNp}I0(6BM֢ e^zN7>-KHPJk(-q}aA |BnyJN¼mWRy W.^Xgrzv)5ițE#ς/\u;%hrJ.V[$J>@^I _~_7c,l|#ig8 s]YW'c~uL{ϳi ff,j o{&__.whs>mioŗvy湮6N>&3n·CQZ+9{ )sɒ!\Do[ГYDqk5? JɗpGoJJpWڮ{+?(_M֒ܰS%Vq>2\M;u%pkfm] fqM]8K[Yuhkt W[%Hr6r;ޏHā=(j v9P`.U۸ݵ-cEJWY:)rjVTK%HrfXH~C7nXYzٛ#m|&ۭSc#%Nz΃X1(瓝u9rRRӒs%O-APڲ߰]6Cg[AA $M* _0VȖ ʙfao5ks 9 O __|˂^>ŢA)|\\H\ֲ%Ag?=ryRtKe`Q!7GڑL)BWrV*RgVr/GmJ -UKJJ"qP]y{Sz<}q>P%xݯx;gKRbRO1[YN'n 5IxD¿ҐuÐt̰C>Vk|*מ/>{ymʼlȼ˳:=Z~]I(*I5|h TkWq5iw(^A^yS<8<`=Nbq_85vG{TAq*MTWSK=NV]NL;UQI3c=s&3ۛQxXJ_]2SPk04Z%wv7;^r;Z\Jvh=9-ASr1R3`k&c i/ݫi5ɗ 6=zRs3R9<F$=Kiw=|6>y,W v]P%) JIZQ: x,'LpZ3`P|d|k1L S:ʁPx ɚlgS6_ȩF cKPtT b[$|qkqI իh' ? w{2bA9``'BT7A =5S%,A)GRD.5sdŋA::Ӵ+T>`1g 0ѹ(>\npdF-ʘ2 ٴビk|)|p i4?(>;ncvLK2}Ҍ eK]ɬۧY34V|`O<3s}L4͔_ɪt1NEFOJJ`%9}*>q%uj e=?3}u~HNA"Д M`7&6s516(qU :&|ua@<ƦrP~/G0GKV.TT?.E;)J>_0X*eyݢ %es>ǠDBNG0GKД3g(s4` \Kwq0^by}p}~<\"4"eR{`T0|=-xr=ɦ>>=9kOgl2 aڠǝG?`#(mg㲉ty%ݜn@FK/fPh"#Xá 1\1:;gR8P 4vY k0E I SռddAHXx(;$h$S 6T`![u}26«I PW3RRZ/記)AP$rvn!+/i G]Pe6aq8rSćg{DP=墥P+J)FF)rӪ!JuQce@ы0R{1:J*$\)}&!n9ͬC)IA^-MAIAL] *>EcBAUyf0(q6D1G "`)$Z怉G>h4+rh?Kl-U_\͑Pu_ܠ,/enC#6b-*b6w(gRetmsl4OW A ea9:$HCH,6W-AR6ףhM00F=I!J\(7`yX:C[ͱLGNH^sǭ(%ͻv z~n.Nd. z{4]} es8f)a2p(:$S:,"=zF[gh4%bo-gC] {Cϻ,/ɦd>lccR])-Al9zW$z~6԰ VVȴzTttHeQȠaP L MI _|tQ2߷EOH{4n{0_ˍ \ h9br 3W=(#- 92,0›Cn@ꄁ{aX7U2@ZdG3 iJ%aY|Y iNW^L%i]))A 6(&pW˅͍Y؜b͕;-+wZ+ݩ}n$6פ '˲ V7e͕jh9bK8lasvHr5OJ[ d\tMfP? R+kJkT.cKB%ASR:h=Zb|Y.BE({j\:+Fjݿ+G:eH@䐐㐓ʹFO(9ƷgάwVbhd ro@CSOuLJj$ D WAJM>֢RR )eH|%~/FtBO *kFn{wkv2-Տaq$>61r@z7R|){AhX܃!A Oi99zWK7-=my%m’Mi9SPٗrlߒOh?ӾZpUEuwyX`N9~ ; G߄ʔ /"tRn?M-3,JBKk?f=3%2ۓ9N&"9ok+Pp lShx PB5^_ay2ܪ,gbK<֣7%N!csC0 e{g;;+8:"/ZrR>arR^\X-Z(ҘpSIa>zW͗rg~6Z-ABZ5?ޖqiqB^7~evD۫<$ Phe,K>*K  M!`)Òn&#UDZ('=WeqUW,ke-`DSߵ},Iit^|!,w~l+?ؿ$(_r.R{RY+a1_*.`X|OJ`B0O'BS,(EAY㯒S|&0_5.y[jYSR0qM6⌺0%/K%H7_r5 ){6%7WQeȮ%wˊq C/JsܗNzqQS"?,IڐsA #Ʒ 4FG8QMYO~H_'7>5arR6Q|i9 ʾjhJOH>tBK0z4r 3BL. Bw{-AKBd5_],K;OA[r[YXyHЈ[A (PL6hЖr_ , jlEK|cEVÖ ;[cI}s`hE7RaCzybpO4i5E^˗oR)b͗r/ :P5?>Gs|fw&jSE#wUo+K(fv%̘H{"fOOW8z8A/hJ؞]d$_ZZ^ ml;\3 kd +iˬpVB+lj)hPHb,v2klybLAe̙ JK;qM(!A^9l})% 'o^Qv>Y(RH_``H_)v;6H_)$SDsYq[4%ԃ6M)!ZM(] [حRJ;=Iث4W,?mjҼ@&FYG\&ķRJsp\P\r>blp\}d9> %c3iI}B//zB9$p*N|rxA78cnǒ:Rͭ}z`H,L'ul͒ϙ$)`7ʲe&_ u!-=)Dx|\7% V|!'ѕkM%͐(%gVj0O Rݽ5SPxd1[T[wS𙔒RNjt6 %a;?bKղy M+uj61t/H &Eqc?~ˢM̓",E.gl*&)&t_gS(%g1]a8^.G \ʞ6Uxw;. 3 KuYi Khx}3-'A: |<˕J>mP\@'$!lAΕR+&'2R6Է[BQz^[ꡞ3(`d鑽tP9L=zJi= OoBJ )k965RH+>kw˝'\/C?(g3PL?`EsqRdI.4A(Cb@A7aOVj=4 OI?)\֦7Yt!+IMV+ӵeW+vJsӽ+w+O'[(ߢ&%8 Rr[NX'лn9] f,jxa>։+Iv(mgr7rWmyẑB(ҀA[cߘT+g? LYWBљ'U MFh'&ӰhJoY&0&9iXnB_wY|!l5XLo{2#MuXWȸ}fmԵ oe~F)|Y@U9;M:ZUpLaUǫ:MgWvuln8$'wTSq荈V#"Zd#dzd,V_zeB3oѭ' 0/~!̉Bx:5j84r;ĕ@_ BC%ˊOAEWJKeR)#%Ae{5y%0I)y`#8(M~=GXj#4)H  e5DEniZl7֏="M\KPz|*=[%/5+ 2MD՟]}Jk]Dp5=xQ'zn_F ,EEO=BDž,*E&c~9elT({'pբ'hz1c~0@m~%DM:D& bq6R9فuP"N| eyG2 qEMBe >W!VR7&_ʉx4e5?_YUo1֨C]S70**тY۞ pwt; ʜ .ТudUҶGXm:'E!0\ 8TyJ b`hN3~|Pl^*Najj\\)D(@/?o9qѷgPJз¹x0:{'<&1j[%{Rp7u!jpEN4aL¡ӔwoОj*qX V@5)xI/5p --|)2G.=%gh-AZc@b?>^ ˰N[Pu|E ZͬJ US 쒋?F.Jzq{9)~PRJiɝja?TM9t+ vPs(kGM66vh|| /rPG|ӎ(koPov.(̞#srJsl`i'ZEI]$Oj+TXCR<tD !CFXqpu` Ad 9ltx̺Zk e5?׻Kߣ][/ BC˽V{4t)>Аv]`iH Q3( p3''*)Y1(%eB7=9~Q)d 1qчp;|ڦqr2|6CT 6yW]%t9$BJ|}]ף/%P(ޙhwj+{;'\"Ah.}]PR}s)j-mwclEb&A=)-$)֍NDQ LNM j~nGh.<9U."]5ߠf\To⚏^}FsZqK$]5[Q0i\/D5z^!I_tQho1s< X?^q8Sͮu7(:\Ι`^/(э_ReM6]\谣诳Qבf|1v^rvS޻z:r ^RS>Ftj_y>F 7}A٢&(հπULg*pQ>PAXA9-ͼwW@JR p^9$r"`U}R>eX%,%ﲩUi*b[L ׎(H􁖃7%PUG(aQ’ؤ5u Zx [yE8/H /Cnxy6'_B;72lD;sMLH#b4H1Hݹ(it}8]-}{r$WA4kl5&]H`RNAxtK+Æ<s`00U|3誃l,VDL?n3eQtIw `$ ӊPR$/HŇͱ1HjL)!!~TꔍULR.樸g^u9P.&ŬgE-|iu%g[]EFrZCi,W)˰*)Z 7eMe]_ՕT.)¾߰Ut ІR,CG'©_~qQ`OA^"Aq?(2xwx;+ʢ=zQJ%[v \r{4B^^ 5jpL>_?kQ #FyUmȳ/>{>] ԨT2W⭋b+X~l[},G^YH%gi+~moko8RV~[]&ƴ:. m &[PU^gbCb{#M>yiuHVw_r]6}/;TZe.qhvSXH>4NڢW$_ɩc UU]IB-XUt[j2W1^*=!n*aNČm h wt_1zQJ%'xʡ z~h3}ǷK= G*ho1,-~y^+i" yq5T[c˪f4Z* ݛ@\AzJ-D4i]I9ݢ>V{49NObT sKP' :ih79[0 ߬[qjȸ)-A7r嶿n 3kKيޙbPx UUJc [^!D7pvvD_KߔًuʹDA|P,yѢt֎|4ol*"N\QtXwTJڍ·Jqи]U*FRFsi(Nr8JqˈEuy4ɇL!-@VUj.W:\ѣV$26[8&?Ŵ̃ ,㬴(n Y?(rgoXM@eC CJ?!2&  ~V8f疂G =7" 웬jnuMm䪞`aML)AA6Q-pL%0+KM# iH;p|znq|zƾG/JI|rqɩ)Jw{zI:Ke ^Κ@At U C0~(BBԊ`cuVc~JJzN )-Μ}QR /֧|pEw[RPE1`#}7HɭJE%|űGA^/4}qE 4Rf9:G=/NB.S˗'ֲ^9F[VͲ^t0XTIIJ pfNg-2# @ւQbꝅ.JXrW'$,fO{}s#a#GX2f˳dѾ<YwϥkCE ͢PZɩ[6vXlюP™U0>A7J2oB>)}VR– z:I;I?[j8ֹ>$[࿳K+eeݕ7EMsu|TC]e n-|ltV@R5i%_ʩcYRO9ĆjJ1](* )tvQ|S᪓ 22؀c#t^%HrgʤÒ`PV3/RhWRJ+f+{]nj«n/5ټz7x..yD ~h~w[2lf{в׹,=^+uE2R<'m`B6߽+.ɒAz-q"(bp[U{\;^L֘a&߽xd뉜b)W_R # %L9ԋzD}L~ZZ9:g"PFKEB xnZ]bR2doՍ Jauݐ`~w3howX7`4Eë`tPF0:f0ZfoY;&WVu:)ASe0:=JI{ϟ__3OuTuՠVgZJ,+SY]g^jg֣5![ec1),8Ӻ;9:IDЬz&TCSi(eu%ARV:I{~oRxy }Dt+ ޺"JCYE(#]ߜ62 l 'dJQ_[$בWB~vQZ$j>ia 7?)f|-y:08(|[YLP|cktn|A3mnOXߑw~Vx;G/ `DƋiTO=,1_3_v'eLO/ aw%٫[u|WRȿj}pᎿ QR2A?&Zb~%g0ozJJknV)AcsڣjXn+^W}e+{q_jmepj9~??Eow}OߒW_H%d3ZEt- :t]!3'Z܊ZkMNT~RͬђCIY& QI7} Лn'˂DsfL6kd}etEG>rj݉+ 0Ex3Z3nz85҇ഏBf|u59%YR9pO3E@G4rQp#s6ӄ4I}Zi,} >ZX} _O|]7m",enK*3|RT "F滥gVJ|}BZ{\wlΎzEǡT6`ر)ڥUNnlE<83‚4"E,zmJI@vI$EY|\l+1gCwDpR,gj4d4ND9NXX<5%˳b![Tb{f 1zlVLG0-nj^o\5Gq569? )(QPR 8;yRt`K* % o-(.v2%,燗lf]Ο~HzW\\bf?W(uP\xa"HN"SN)ٽUƬReRZ~ׇٔRZf?WöNVYdNPpJC~bp.l溸ڜ#u;| E\@*~lT}Y a$ʉ\ ܯ26kE(HSĐ,%F%Zh-Yԛ~X8}t:b/dQ7h{~I[uEgV($ ~PxZ:siE?W(dI=rQZ+d%cgAYx\DMsU N~8`S-Bsmј =RݢK)w f?We?עcѴsH$$?j]{0K[3ae*Oì7\:dTMq\·ku_VhdRrP8~P ~\%&_w`#Us j~>}=>ڵsCIDnϕ96l]! gmd$f?Wl ʝssw^s㣟kRkel%G*3 +jvn+>Pb5iٿr/{*(\2 =KQ8?`'@U/2 5j. 8j8_igR-\KO[>qə.I.W\_h颻':`yK,^>\eءgg9೟+8Z|:I90>s]SVs]9Z2Mhfs~ b"_.8C^f! 6u?W {,6EGsOPB(fA{UoVy+7;EU8uL;cjJe9;e;m%a77T7*\ {^[8(!zQ72UJZbk|ԣ')~9c5V)ոRlp,C-.\+'8 }5(UQQB<࢟ pTFNE(aU1c%,)MKpM_^eA=zk\IAћh2)Œ9_0#ĺ@ X>[֍e`_4 բ$Hʡ|^J_k}д^;=|@枲?o_e\VtU^B/HKӎIխ]5$DD#%ZUS4e=?s-}X4gKJ3ȴVW5T>]s~KH/' P:Ȝ%Lme B| UH|)謩\ PHHEYl.71kK[Yd{ť[ְ*v8Ѳ*v1)f=~icϵ)Tau%YVwYBuɩu#1nV ]qޠu[ܲEWW MIP|H6GzTz|^C&LS5dwoS^&=X\,:ѣ$pN3{KNK &e5h%Wsԭ(%ȯײkbX7yugͦ$ o{f"Iq5;k&ΚbC@^%'g~-g`,膺F6l}KV9cadzXA"m#̓\_}hx[FdOW|5xXÀ6tNJ}(wzr9OrIw7l5qlZx_f~Wow?~sM?F?$_rw~)*xR\CxZ>E^^Xy!w! 辐נl@^b|9/q < LtQy5_#(h1z⬖ (g?M{_YkPy%hI )/ ,VWy*-ؑ=w:o~|Nϵ 3څMСs- iG+4?%[?צ(|ySv_sm 6eʻ-쯨[?Wˡ"tBIAygޞu,? [؃]B_/94I|XVV?6 <7\][snkjUN' 73r(*c<zP]  ;q5Ry9&UZ՜-"'0W]}/!Z1o1srsw *CRqw+> 7cPHR 뵥=A"$LܸZFe ]L45ֻ1]Jܧ@-#{ZtpmFΦa D"̢=KQP[1I'Mi/ŔRh*ٹ ;X23?Vg^O V7/:Q~YE=%ACj$:u2G?vN84K~[gC%!k΍uk %jSmi[var1t^'}}Fױ[]Ku []u(ouױ0|u֣gT|s}:-ŤΑ92eoG&=Ieɠ)c^juYbv Ȕr.LAd/_V.Kcv\Yu3\,|uYRݞnf1(~Y2FA#D,?iGbUZA (ν5 },uYR=ƪSQCSơ.`s :VV%nuק$,u,>B^e/#/K&.=.Y_ыVwԥXX_IW}沦[1KK.KdC˒ V|;%u,/K&:%c,eI9.KX^L߸,)}:_G^B]=Plj+5(8Nuq8w_ JNׅ#-_=QWKP]ZCr|:6_؝zϼmw [<Ք,e0|ԻXݤ%kXYwK:7쮨v'vM%ٍ[1Ė&[Fi_|C[a~1 e+?(_t/ 5? 1MVS>5_uT۵okC~I2;$'Ĝ|_>_g.65*DFү/X$ByRb^qC)j$ކ fk-tFb0p[:wFxٍƽǢaw6E3F8N:8E'a #LwrXv[_ Lw:w( zewoyH%mߒKvicCȢش!w i6mSsRu?%zC7dQ,r+XGQѓ'O<@Q&OIF]]G/k &x;0SODTk;}LK/PXߪUR\[aY1n3ݺx?!EQ K>Rpyo:?-e{X̌ȊT/Fj>Aޔ<>Dq.%mc~n9L .I3ImeW9 SIqzLggQY 4MdF0e䛿av3%LSF˼ɩo9N`oqƚy[H'-tY7(i f AU暶vyQWGƶ7$TRcM5zZ 0qbnT~GoSKG_2FTUpijT?OWgm+d>4R)N)|bpyCb.RNlED?C> +PFR%D*[Iɨo2=r_lK56RYezX0bupjY.\C}TcE#FUz^4 >^[kGu&=C"qtK֢zRJZ0Z:gg_4uvl)((U)|`'m{F>4kPq?j >2^IR~P+)F 5})x( ZcFpi*/w6 oCalES 0(v1D}|l-XX4*a97sw{0Xd0KF:l6k|\W ֫;"R+N"WB2jd=S"3U\sl9\m7RΞ_ͬх=8t1ڐ@Ï[yS>I!zE" ]U葫)xi rb4masRw'%0IGGvyz܌G c~n2Wڬhf㡝G}mѡ0HJ_7B͢‰-Ehu,EufQ7꬛E8U-E5%E_w}tfQ%gz{em" [ E)phUl\xP4Ӯo,%-EDGs,*(YTDZCDV|Εj4?i [ŐX)¦6M2ACOR+O7plh{%Q - :r0Ǿ<-|[gEq>a6trFzSם6]zs SfaJjp>.yI7Z<8y^p(?pP+j^@'qu*5rQ={6Rz 챱=iVAS.;A8)gd, ouت(7s!&cE?HBn.7#"oژoB0H٪~N:aL6xVtsyjl7L>ٖݩ_9콽O fXYQLգ8)FgƵzyG G^pQQ|UXwG`RVsIs}!5_B@=+ }N@o 8=N@{t?Y*L>#^TH Y]H͍ap #A(%+)7>zH0(]yUVtACW4:zp*0GSDԥ$s]TuqKڃ\&<9;蕍VcED}[c+)؋z8BIi))ߙŰd]-*aY1p'2+pLoY1@Īb@OzyrHGD d+9:*$TH0(?^ʞc j֗Lbڹ\NeLe&, q,+Qbw9/KT`,KnJe]S.,NZё*JJ&fNѨ=ĮJ g:[? :;y7J~ɳΘxF-ݓ&,'f!~I4Ν+u9݊Y&7܁.G($(G/>N&ڟ7QjF Wrt evyx9zQw^.(_igr,vte]SW!rVrL~)!Y6#%[`e~F>nV7(au 1zQJsX]5;+yfh?Fr^HeP"A peeWTz…Ut]TsM l>zKB)g6Ԍ$RYW0sJStqM_m.fK z.&|m̊ vs!"j csQEjhɞdV=lV2} s[Z]!e?kl:VX?<:d)AA)̲$(;l9PN#k+JY]oYr” NkΨ9:=V#׬Y6Vei72ˊZV~sH9=ՠ9S20)G3^~^@y 9=vG7U4){4 ǷK+JO |,@^M "8M:2Cwk"9Iq% g+p2AaX t922;렄c:-kw,)6J|8Qh)\{0ƑVrdsI39X+6ΛM^xz~:dgbm7W)Hif|K'%Bq # س( ZKζ:V@-|ꊺou9:6̰w+ .빥c3m)3{CmXeu%簺&Z8:qy +jYE7um@uޕx_TgKd{K/߲|WF/JIctvA{~ endstream endobj 36 0 obj 23896 endobj 34 0 obj << /Type /Page /Parent 3 0 R /Resources 37 0 R /Contents 35 0 R /MediaBox [0 0 2785 676] >> endobj 37 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 39 0 obj << /Length 40 0 R /Filter /FlateDecode >> stream xKdIR+bvՋ!$v!4% Ō\"hAZ/̎s-=Ͽ?|}>v?~gO{܏;t?>|.o}8'oOM+K0b/?/>3RwxqO Q~0.t^w Qr<] 珃sY`&:}>/Q|rF|=>=}/|N.>I!zEϼA^~ ߽)|<7&l7y^&"bxWv| %g?_suȤ|oI'=IyR|O|<>sWu31L.%[lv=a>+ x9':4#,4>B%=)C`wr=t|$<. x;p1G8_R.)c򅞸kK\@kДl(SBV- +3X!nFr7L|V1ީd(0AI*-ü%;6o'V7(Ԭ<s;]}V.0()ROtlnKIkv|2n%}}NgeOA L͗KK'#HqDJ1|Pb!i\Hėsh g=j&b߄RooZyoW4# ]rCx.hvһbăL>i[J v<텠UѤg oE7A򛊱*ĖUmg[-<-ၥA[9 CkJZ[ ASзbvR XXOpV yK+VlޓaA_KKoJ%+~i0(ytrjKFї杢oiF{AY#{w|}%c޺o$r+=Ň*47,)@`Eݛ{)Hl;uڹӔs B_|/r)ɛ+5hzN5jhД3xz%PoWy{L.sXcK.M]f1S)&mjro[g]+?,?@.޹,+3A- ;$A7wPEKu9v]AR> b}|%AX?ηXie=>;ܞ4-dw3ϓL߱CjrXNb1͠8:&)愬@X?E1ABpB IARh U 1>cPyb( u$G\{@$>} b/DZ#Εb|R~j. [Jk8cv뇿}miƪ;HMˏdyW %v;z۵_,hJ+$YCs[VzӮݶXJ[T6'Fѯ󴃬7Xv&qsU.|I:VKC[QGib#.cPǃT>` AX|'Olo2I!lG*\?(/J ŗMP?~5jfz%~ Ie ~ӓ3(chg5=nRp@䃯5|c"$5_-RzSR`֓NH?\`h5hcGVgg«, S PѪ0*NoR{O /\,Z? :n>F"nM+r. {^s9Oyk5Jz813|]-)`&%)W]s4%9um_ wUxXRGZ,#%-ӻ2xo\bĂ65-b"h5}g€#_w2 [ D; ]f%l9wy:_/=k|6BH_Vi/g͎⽰ y0ȭ~["A ?y Y$%/lԔDP@^D^yz^ [pY 7,$[Y۩Ν|5 7WNUpO ),I1'_"Ԁ3RO5o&a3Q?Mf߲+d >B;K  K\6(ko6&w Jc| ov_zeyWk8HK7Z #/F}]yW%|i G^3_)X(JKc罡E/m{4> Jɗp7%5>:L&٧𕱶ŢJte=>8[wsO;lev& gY)@ LL_MxEel r:ujTIn-|9 y^IRb}mv33)tPbi 蛸>Cosn/b1(X/>|f|NZ\,ZXF_joQ#}dY~#˻j9(8kX1([cUA}d[9-h9cRoX>k[ 2ljoXG4C2])0(*5}d뙋eYwF-lqJ|^klp}RzK\wT,zSS"'_#qyחKYcwjrbi%C4n2N +QM2s >\KYs[}y-JijS|)r7hZpCCB+֘46&ĘBy;؆}S/YJ^˭3JSX{lp,AAԠRO=/3aݪm͝{5?}y9VL]oֳzl(ɇ"8#h9m?/3:7[ 9 >MޔdROB:'UtkPd>A*_e%Sɉ"hS*H_ v@8E%@ n6|* |+pBB{&6-rhE$ڹre7q=|fϙ"'8lq)pY߸/^\]㎔μ<;>yC&N݈֠ůvy\_OR|5Nj3q67h\+zQqy^D5T_8n|ty'$R"u9OГLbwOӇnRa%$pJͦQv3.w,u9 Jxߓ5$*%ZZb#n.8[{9qx3Oy.2OR& 3͓~]P)SJIZQhywyf :PaP"|{0t x j@ o2ϦFoSZ3e3>[T+֪w[,Tj)|=gnz v;ib1{u  ̓ݞzl:{ p_bq h=A"ԂwM-U$ݐy'&/Ã24quug*+7Y܆؝PuPX4 ru]> #.gӌEa\8=R pR|\8V8?(>:U-lYK-QN=! Q->$ ||pX|g"t{ܸFg/kRB1c`JzRRCƗzb)>qtj e=>smvn7"]nDB@˚klHn*=n GPGP$Qc\X-ux~:Τ\@ғrEтO6"A@' ob x?)P{Q؋QaeoJ%Ĝ$7k'2^"κzW)2?POSEʝyr%mˮp)!h Jw!Wқ_B:Z)kZ,I^٪ ^0B<<יy|s)>ḄU2hjat X"0 r&AI }Ηz/<@BK1:{R}~ "CLxU#” fSo |Ȇyg*nOX3MR@q.o[V e7ş^\H@?==4l.wT9:f\yk:D :3Ng~:8cz*>l9ʪD >"ZƼ_=!؊?,» y>b@3'K=/dWXJ )l>T~J0|r@x}X\+dڏ,ʲ/Zz(#)Vهqda3cJ^qA!ƴ xklI:Ǧk$2f˾} 3AQ"Zlq GT,Ai&48вFiCƗjy)]0l6yf,LEٹ{{L%gcz`~H/B(a q ֢7(r-SLO6O IMDvJ| ,9ciI(:BOtY/5aY|ĝQ Z@t#هP؇nEa3ra$/pì&v}ܒ3}Jx@l%3ݓuh  pHQP3|zIYYֻ͢z?kd?4]#rRsoғ~)',Ǫtɨ~D v;'LJM#Xq%Q䫪Bq@A8FSƖ>l>&Zp 8'BӔ[fP[X2ɞ=6бPl'_PQ>d$*I8(:)/8؆5VOhP`"vkۭga-JN\8<"*mi dVOU2_^l/a&Ŏ椉`MS3J9 ^SG#>duR L6pとmxV>8 ta.`z|RX8cs[S#*aMk?fPxr*i4.pF8Քi$m>XPdq܄|1Yqy -!Ûl@)We $5n2$e~i´pr.:");Y$O?o`.8ty[P).uh3[:=))WҥdB@ėfqW1TǸE:(=IɗzjtV|M`P3<ڧsZ;05GJ !@JURf cABIp~e >(AAГ92~xh* xGv.>jN,m{捣ۅqHW< 4n/-4d\7pI{JzР8 WǠs>Wx3|nܟrdMKB ; LAiWb!gP(oaV9t6fv|Uҳr5ʉg)g2&2B1&$8*BjU$\(s!1Jqdj #2U7 nZ=JO=I]B7*߫Qg{\z2q9Ț)< wǪ +K ɄWSuZ_U5(::JkY)dYٕbZW JT 8Aئ}U|*s]'}|ȸ8$_ )vEQeJu^CD*K6޹e MwΫB9qZ:,@: tRzpAmiBSh+U r9b X1(#-f0p7AoBh7U eHCe~^8皒|>)Aqۮj'czbNU8-pZ+þ}a8皴ate>0IJsDh9ʅϕ9 O&(|UX/D7l%&zRjoMU'\:Zz`S-I7㳩ԖXN%&Y8i qʠscT+% b $)̟Nܲ.b_ nDQ j1 uQ2^|P!F5;9U3 IEI*Ueq[5CkVR\$%_ϕLMY'ei.j,7¨~3Yap뉗ͮRRb|F9F8AaB̀]Ʌ > }aHJj)SNҫ 4WRft۳yO_@huѵe\SI+iH,n07؎.\rkGI[2`&B 6\=ATuIuc[ԸdSwgjRكXM`P iog(^T$;E' >եq8mwE?.C@>fx.PK-5S:PDwcٹܼ2h”'+Y}VӠY#W]tIENT<(H&q`cl(Z- FB$.h)I.׽dld B Wf2! CQ$jYxUu ]''hԺENw{0AY~>.Τ+&Zx|rZ3]?ˑ/&ìV2*|_ Aa^O\|X dX(y|f.DXLHgV%; )/TJ+-ID96B5mqQ=3Ȟuڳ ۢJu[pEl=~FcʞuGmlQ=VPn+BOپ* e5>EiC)+v8{=IծQɆWy&ܬv98uv9,jo|lI945{f.a~bFѱĠrW+UR];b1!BbӆC2B_@Hj GL2%E*BK=-e<$׊_lk %ZTLDY {~)0 L 0*BdnPJ- i 0OJutV'QwR#fPE<^ly~h:eVZ.(L],E?M$4N,Jf Հ*ǷBz'S'OC9|vcE|]L ^=߽#x@߱e Dj sitfW0> EB.t̩p()P%mt27'Yt`zMBMkNMIwA{beAj$ >aXK/JzNGJOHGV>|qd9`ؿ;#(71%^*z%'et}:>2JO )V!Yhg~*R}=zˁђo5UA, hAQ_Ϊ衁JO,(&=RniѲ'mt,̸&exJu>I@3p4;/'nhlsvI1|.39 b3r}jB:JhIZg?s"epE8p_z{ m6*{s<:} nw+h^exPo[Pܯ"?(rt7=/O,/)_噿_ȯ4"sQԖfq__eTs 4JW_jꇂL#AbC]%/J5)~ଔN.1@^,.H~IAz| Ug X zO' AAP4m>d/=P. FPbYjf| U}K^UhaG(1WCJUk)ͫ0/]O{4+u ߄QYc0&ny_\;~rn\)ycpVyE8_~sHvA7S?DIߑ׿jl6wFW8_oAM:{gu6nB' Yg :=:Q!w Ji4tF. gK{jgu3w;cqyu^vu:y?%zKįb'(į3%oJFq3ߧʊőY. @/bnV}I%~xQ_KlePqcw?h?6sp F84)XK"yoƖZjvpj(RwPlm،xQp at|0Z`b㊅YQTOJѵNC")>1 CqB6=L &h#?E̘_讱|#~DGɪzB>ҵLU=a 8,^=iA(V#y:"[^˲Z:i@V ހƱMQx7/.jd'Pvi8Vۥeئ0umuX΄j[TQML寝c[6)$>#]sM@ϼX;`38?αsnY{-;RD{Eآɗj*tv94Jl!Sܤ<\^I2̮9c"+wrդNNh!5]0tcśF XhTRZ,˗Ӡޠ|e`U[l_*F'8 ) t҉|~P\S\96N,ٷ]~w8ߢ^%rz^˒>%Hz_ߝcykd/35S9 ŨoR#,~w-v@į`{BPjt 6?(dǗO9mJ:YEQ*{ޒ/IIy]h1FR63[)Єͨ*Qwtm پ(slαw v=SM$tu: t:C4*;v, T^ YX#VHi-/DOq W@[7 &NFCO?Ʋq)R@EKzAAz|7Է3R6IQ@V?͢TGj X rKu[p@ /։f.ثYŪ JO=/gȼ.[I{]ױmdNM.>:%L l^{fYd:IJJz]-.w֠06}MM℺yQ/G~S>V-r„@|l6I qV#[e'q\Ӟ3#s6`faH_T*<DI줺ƶ2NȲZWۯ?"so?\t,<>X1OSu5 JX'nŚE]/l`!ؚA̧.b~)_NJJhdgoCݟ}F?UCtPWf ~/Ym7YK_wP-@uwNvPܪE̶ju,nRbaxYE ~Wd[{z~WNVK#VyyYm XXbfddYA!2fOIK̯,K.6_ r F5k̟4O=?-M^Ov a;;J F'ˀ'dd3E$8>_!?/|oχϼNOv^ b':t";C5BnJ >5A96Mh]XNaP">kWY}\uֶl쓃ȟ@h8CCz^?al$)!f}xwj5~|SOA샓:ߑQESA<2hrVB>5|0h=-j|UγuVph>#mb'bPU0=9FW(uPTxEXT%;${>(f+Pm6Rej) x.<W{T ֹƅ+ C6c]lNQQ[S1WG f!0Y[!߳+\ =tTHeעTEaU0*%. "B;w[!EeD$+IK/*+ &e5>M>G6&-t 5/5D | 0-=U :;>|'f@qm6+$fW(O^q[I>O{ rtn3tO+?.WӁ<[qƦpqmJkz8q}loU|Ǧw}\}\9X=?)eif[fLztYF237[bf~W:Yq%Q8&uqm>.v{=AhTTX+&k)|yqF(/fNJqm>jVs&ƀu7a52~mZRkW_d}\YV@u;. 5WDFsOgW(DeWh03eA!'ܔ\ZFjV.&x[#Ó51WTxeT w p~][UzRwe|+~ۏL7ԎtՂ[E2i$ Wl:|pU_)yM88"1)Eෛ/ 'r^^$1>ܻMq-Jh IzSv.n`PVs[)㵝KFjGq}\Z>~5(+!P|¨S\q}yZIeWQl#|$QuLM K'i{fߎzMYb"_f^a*C+LŲx{{eLUO%;tII4,tС3ɞiVfOP {puWn6yp#*on!V#bͥcT*Cz'S ,>kMMv4j ˳}ܴ8xSӑ7:)ѹ#;U0^|=Or:i{ȤPA:.'Ȑ^ҀW#'? ~B >|qdjmj$=(ղ5Pk.eRn0dU2'N]o!P+=ҫ_is\q1/7<l9}=x6('+G>Fhj$E^yq衁uUI|tk 偾B@eGMfaKf`G82kSHd1'|f`wm}5'9L}\8'kS!x|V`mwK:H ̢ t1!N..ka"(4]|Pj9_;3>PP`tܙ@A󥞣fM`NBٕj&^gы/\X[M뺋jxmvw޽/&żg5}6%bz]; 飏kiQJPzj&).{[.t ^^u0vvScgD=I Ma=8tuqdž7u}`5?~"9-z:2poݠTb{!(_Ù)gҥ_Q[}!訙[30Awe թo$ꎚMY#/lɸg&GP'\+;jr9= Wi|fVYBR Pg XƟx:vhjf},c3g6܌yeyR(C |mAcl^iR I[hcH/ ZhIz*ґx5s2?+ ]R/[kQp4ܲ-PǼ4qJѴ|(Q9kxRe%UQmՔǷK+K70c9/5(fBB1} 6a&"aW@^PTKOIi%R8}L ev&e#J>5ӓ@^)\}U̚I^WCz>&ש9XҤX1u*m>}\~thJCk]*o+!=)A7hƻwg~>U^m{E=7]մ|>rO aw5ۓ@v2{E 3A_|y-u/ &e5%RIt`bƸ%WN"tNZ;%4PV,eW4{)#t6 r|6KQ|XٳmP%}y11,\Iv+W? M{1(q$|4ru3u08|u$ҳEjRO$𺢾vHapoJ7J˭pw*&rԔ*_g(t#S`RV(eԖ&a k^G".SHdžK94(.:1Ы4uuy¼bRpdt[^R9XwRwcE;̢Xc8PÑ-V}cA+eá-_}͂K-]5jy6uZzy!?X{a{{]vK69ˋuIƚ9^Aa%XuˋuhWŇqU!KJCtÑR5udDoSȬ;/JEs8lTn.r7%{'.?^<}pD6^t?)ncy82)g,pdh#8ncy82)8)}6_G^Y6v?K,j.Qꂒ[@:28(_'h!=Qzncga uYN5~6 J$nX ~ů;Zd)Tw{}'f?0][v 3ǴIqK>>)Xߕ~6ud5 mf *Qlڟ0~-G$/VlZ T~jz6#?sn; o1?aDȘl#N6 &m)T648)N_NUq<78Yx7Hu<)6-'EҠʜK,ơ YiDoi5L !~HI6"μAIk>w#PHPt6pa]^6/ J_^Veg|1ŵ /F|lrX^$lR!ﭟT_7odLuOP w.=+c+;%ӿ׷KCKAY㖏)nyFegX$ְjږ#.MP'Ud TjNDco.ȸvN%8Ŭr(,uúrCS.#)4jsŸηrքRbÇOc;Р)gq\ڷNvub0G+tS wv ĩ|SA+Ia\|S6BogbոHh#dNهө(u=fwV6BfNqj$ vbj,:lFq-<)o5RKR!:lɯpi$6:(1/ʚ*_!tۀF4m[Aa!Qm&|ɢqT? PߤP@i5[ܪ~M~P/_HW6R/f&*%ه)D%b ,maN /ERSlq|B>,|+)$L~*-Gx;s|[(-֛E+{!WE3͢fQAa͢o4͢YMfQ76WfQMfQŇf,*d57a523x青 G6LȺYܱ͢k;I2.Eqe(HY~sYoU\YTV$zC㛰oKfsS3ITubMIpMfﯨvnl6nH Jƶ9XP6uoWF;]PLeESJt)O'&fiL݃?7 8SWG诨dqD6 Ȭse̚w0 Ol_zer exu c/BqdJ:'@I N˱)niK(`cJaWvLLPjP I?J^PJ~RmJuVK:S+}xӇdXI-x=?*%Q_ژXl%;8ߞG#lU,%/Ao/l$11~ߔ2F~j^9/s+U*=t/ܕ& M ApKBt_~cBJ#鰇 Ņ B?EJK=.eP )ztϖZ&Esb\/t8. Q fsb((vj&Gݲ(*e8ԞќX?y-I5=ϥseD ik0)񹷸-=38[6]eMz*M@nߜľ!RRdŠ'|X^eIx]: 6;r_y^oSknIWkF e5>7zjPV/]4E-=~ɧBh/pهsVRs9EDZWMs:5ٰ%EC{>˭L&9!)H zGgStC)Jdоζeys(uslG"8=<-oP 9pGunǵrꂼ5X킃|%=`P KhꞲ|zMm֬O(!M,g();9WPW܂BwMd#ےWH|ݹv1a<GS8[xVW$*恝[%Pq}ݕ0кP:֏G 먚 NR;>8Z!¸?t0|!8u}/=L#L)ZˊlO ۦE͙ޙ}'9ߜ <`*ZxSd>:]'QḪ̌u+౔BEaZ{Q([:wcǪP'~V.]5(A򥞊x)A)o**%^c󹗚yΚϙ㲹01cP]?g &SoE;V|NUyp`cH*>a_QP3OqcY4^SsY fW]=,TK4ofM귫IJvH3/ fZHۆɗ8"ߒ" =O 욺qZ7#7Km_<%'.kaJ+jyMt gG:1y{g["ە[БH9QQ[~;LGd|6zJGwr~,i5PFmiTmΪ +xQ乱_Y7RSMcN25ntoԍ#Ia*5Sf)夒RU:U) (H*=OY5uZV&+, -DB']<ڙ `Dj6X9 {?Z+=<[nI#U!_Ssk ^'ڶ9˂E,pc[9 hs|=nuau1E3ls֔C,%mJֳ ud*±?3K:^>(^J6znk\^WOKO*%KJ5|uz" vxuq?KxF2=VS3Ґ'/ vʡ3KO[XN~i,wPV㳸ڷ4 yG%x#>Y0[#Yy;K䥸a ˆu/Ϥ$ls֔p!}Pz|84(,Vz̢ TϚj1+J"^`k-U \8vVnsVF[Z8R^JR,Tgvݡ^yݶKS쏸ʹ몙vRLxBt'b9ڮP/G~H/ њJ6JA1at~.X_yY-eZ&뚒^|@K1kc^וjh +𺢖׭+Jx_oaDc,#Jk\ҋq?_\:uF`RV(eԖ&  endstream endobj 40 0 obj 23001 endobj 38 0 obj << /Type /Page /Parent 3 0 R /Resources 41 0 R /Contents 39 0 R /MediaBox [0 0 2785 676] >> endobj 41 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 44 0 obj << /Length 45 0 R /Filter /FlateDecode >> stream x]]$Q}_Ѽ>v׽X/!˂< 8DwUϬ16ڵomtGVd։տ;~KGyy|Z~Z;ݿ~s?9?C ûo]|FP_@_!C_}@ձ5z?nխO_x^~+|W|x{Yqn{[y|U5gZW䵬սw%Q<0E#7S/KvZAkW4%cq~O Y~w_Inao>? ٙ|Y>η# -g\3z S|pBN YnI[X9[H_ @wBOj|-sOg|uZ-{oY$Ԕ"9^.pEqC{غ$6M9F2bhT^]-~JԄ1bx{q`.ğr|p9.ƛ%D7÷ހ{x{C|b o=ͭ]%;+7z^닧5-(́Gq(98BSO#D( e*|R\2"* [1Q pn/ & _x}yvv^Ou`9^/҃Pí>C=U k[\.ֻYOjݺ%mzm'_۰@|l=[p 'x$ ^i*b儱e0GRQ,gxT1+IN!jnm;1j],Uz/> D3h[0@qiTG[؉~+u3 IzR[msg X8ȶhck{cA."() mc~m.Kj6X msMӅ,3djowRN #;# t?VO&gcOx=c I+S٭K" egZo ,_Z t+g~)D$> ZaVZê0JGZlYq#K+"`f^Aqmy U*wQ In#`KO wQb58>jn@nE;1 vSZ ^wֺ&ya:~*,FV'hP^zDU Y$Qv8Cє֣ORXc^X|ITV)3qb_,07@håsJ֧$,rJNHOvRڡ$=>_WkM$Xޓx~e~ݐzJLꇞ(Z-0 S K6Wk.wy>í IxU{ RQɩ IEId=&J/5?KQ)L ŨWILib33!/% ,0z“Y|23%SLŢ[XsqsZ)ˁ6N{uGAfjZ1o8JMpgGYKisQ!t(tL;[\9_\^IxdIfu#n -Yvt(70=zi`Ytoc-$V:k6:Yym[I\XG`P%5:JE SV  I:cʫϔhzmgD{~-H";ٿ?[֞JycOfgntA=\as)t,CpkKlZzcXn ,CR,l"+PEz.~zf `Ldh"ÐSlX{UKhOY%gIASkhzҒ Yˍ=z%hgdl]ȒlQQ^z`Fpܸ31#(lp^ XF0K7n-=z-`B79R  K&zNˍ 7e"Vt(1ا[ܫ f$3UUe%=P/c׌wx\a=0 1+1 r8V2~LF"R)R&9vdh Zӿt^M= M@UCK "u[,q~aCC3jqN S9f&B (pz1>JZ0RFHpZnn3WA9pOXEۻ>$c2l-mVam iN5@NY]F$el#8r>|:`t~b7/qpw?pAH K889 y C/|k|7XN|E<CgV*ڏ~t+&bO5q(eߴ3|#[{"s3QN$̬@3mpİ&// `Tda$%=qQ 8nl??kog M*yydh׮߽!I/u YV1gH*- ;k3k@L,"O6N`5kMԸZgܷgR6:+j*A:_{mkwW6R_'ZI |֋ɫʇzޢĐrfQ"퉈FyL!N}qd~1_N,yl'KsT ;pJATCҢQ cq_)bn"VYq}ezؒYqYKt\u KvkA!J"ݏXͪJ?hȺ!_Cx.&_CZ+ 댪jU#$_8E$ʭjVlm7՜t( S:V0aہ< l:}:?q&Y$4.;q8W ݒeۿc]㻮HY*=sw;빳lWzb$G' 5G( 0;7ZZIZ!GZQN*f8Q>jBZӥx|#|4<֨Y&ũ׎<;Ih&ԓGZó>Wi ̳v඄ঞa bG8\GǶc 䪴`Jj(ܿ Yȳ2`+xhPB54DR+@[/jv$ZbJ* %֋u"'*e;X/jPt$EoL-֣HJAxh$IbϢ%=ReF]pߚ n8<=@xh[B\HC ;9y=c"̐UsJX""zdn]<3SE(YCozI{5=v}y}bAfΧcCqނaIs,|z+ႹEE4_݄tlh]_:[KıG;1j]JuNa 7ҧq] 3Ow/+„~s\kTIxL:~=x]xȱ ԒqhK>y;raj#Ce·[$׊0+x5pw"f$ 0GoI;F&6J^O̙zS a@($zF2Ms 99isWh)r` =9ƜM`"OA-.qU'ƫfc3O'綫pH}A=yCizITWU Mۼ%ؿEYOۚr4#9u;Jf``>z+FCf<:$EZm]8%=uM)G%!aiVv?^밪s񳵧^)8S=ì%00&˜6-ѣ#ƯZLAviP7 %%#RcJڃl/o LUFWW^~egLE{)wj-C(`HM%A}UݖD_z$1*K<7LvUg.N܏X$?F!_R1q%~QQamoUvLCK30>]m; #jm;Dx\ۭc,Wsۿ?эdlm {WQǗs,aCXjݞ#6;Y Z3oλq SmSPlkH^db=@K+Fx,c/IlUlθA|_N3to5kꆌ)<0=#ň˵fh*4@ ިS_HOv:, 5ʡrxP:_[?CyUsuW ?G辯 c->+_D=$W7XÏ]q8SS8%+zSS8a7|VvdDeIE{ʈ& lX)|&Yw{1Lz]q#8{gSs!VI:뛘14rZ/*|1֣֙Cڂ lYo"ҧ^P:P* *J4:ڈTai0l 4[,CĀnU;AvPڡ8')릖Ok/soϨ֣}< Iљz܃nUHe-Sg>>b X˷L]*LbZi]WvV+H9Af{="A׿>>cv`np ByBu#$F'u߅g[:h%U~C?Rf㞡?k{߼g^F,C(D]{kېEa4/>yy_[5\XuNG,9P‰? 3u91PN~ȧ..: z3C녗5K"P!".QO -"zg0I[}qoPQy݇wW`G^)) !wh/%]e3uj{ώ-z=kkJ#\w;qQ[7P+<:b@PnJl=<3pn*\zk9NvZy#} 8Տ g z>+% ڭ }3NT疰3N2%C ?qD\J5nnXؿOkpa yܮ&*#3!g}!?5 ":I8q&'/v;˭j/S֓ݢ _Z"R4 -hcӪo୫x bXKrz1EEErjF֋Y`9-qb}Nb==Z" G;Cs, hd;XYL[c9>ؗ%Ih$=.ڝ+"fIϟ<̎΀Ks|/QX/9>~{;m8%c=\6?Q6Ux^` #Е]ߋ0_:SMUDS*4z\ 6/29vGLv{Y%]Tک֗!ԓ׭sP>&GX/ZL`ЎTV09!Ꮑ-kbLY+ C_ i"w(ҡcrLF?6oGW@K+(M);~\vFYFD!UQiIN$oBSWCdʐEM887c8qΠ $lrazCQ8Os%S54|[=:K-JZfTq@^Oy@?u8|w .? ~'7s!υ\^YkH$U~7N@'RKna>˿Wh%e')p~b(Eyjo"Dw~os#`p$ ez[3a_tA>~mZc Wb1+_A.YKbPY$2ؘ8Qܭ.-U/ңP:91 z-Ȓ^/9s CAUq}Вatfb %5.t*./ "SP6ΈL[PlĀ[Oz}v6xUFx-!| sWW.s9h=W@zƜ['hڙtb,g{AO+1Y VeoeG^Wb2oVbFW]taF75\(#nӈ *i%clb9$T~ Kqc7rH,890/]Wֳ]Ő 9 pKo3Hf)P\z*5\veC)1)z<{jPȓ[BjO=#KfYHu~E?ؘ{[orb_~:w,zx7x꓏雥?s #3jX{[k0dVwkJǀ/A&(g3O<\gJC7~[ႢNӸ;k)6-ް[g~v$U KXw}:oQjj$ڃRzW5BjK YzNu0h0tbpQy\0jCzm ~S38P@$xU{OxrVa5χ M )ޭ2n@` MdWb [uPӰ%xpVxFwsOPvO=n8z_N-@K9K̃~ %&1` bWM?r뇞U|a=:n Xy2^q+9^xcC2FIX谞 n-`vZOQN!uM:Lv8t% v r&s?WG 6u=6 ;]o a;$ޤzOr@ʭt#ZA*xSUS֭V0z[gk;?ѱM풍3YѬѧY[Qiq I4P].$ 7~z[ bsQH9憎a[O|՝LU-J2dXjExȹJ o8ʘ?j7[R!bUȂs%,`XW\6^nTZ - endstream endobj 45 0 obj 11404 endobj 42 0 obj << /Type /Page /Parent 43 0 R /Resources 46 0 R /Contents 44 0 R /MediaBox [0 0 870 290] >> endobj 46 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 48 0 obj << /Length 49 0 R /Filter /FlateDecode >> stream x]MFrWзA"zaM 0z%k]jmEfڕ.&Io?i|i??48_ 94iƷe>|?2~Wח鿿//a?}i_?Iy o_O!AouO?|m^FL'>>bv2[mE>~vy|rm |o#˗0/C_u?w)w9]5>ʟ!<>@o㍗啍ퟸf1F߄ [#`:Jvgkwx:?EO e1eur[< 2ݧB1F|`#s_E,wPwZnJJ0/iii# D9/0kˏt|Ž.tN _wy$S jgZWv J̸iaΩa )Eu QH8ref5wr0(#aop!('{:˵Zn"^_w6†u6sZloe r27Kўۣ:j%8[$ٛ8ܩ!.pHP哆howR$ F 6. ad+HuDB|>]>u~~"@2Xۊ->8*F@gka5 <)í|e"覤RNϣ_"(kmOM0C)-3M˵DV(z]'3#۶Jb/)&>,ҥ cKqP(Gr_)sF )q~'3`[čYGF=fehKs ~XSz11<#Ee^#IiQe‡x3@Hݒ))֔]JP(/ k$u1vkm=VkUB͇R(im +"$$Qjm1 U甔CzfmFX«MU|1O҅Z jhm}0*kڔ.$)ȐiW/2#!5̉y!D):q~?~AS&-cJE3# ԩF kA@|oEK|!m-8+ґf6mN] LFW/(yI$'ht3W+K3[Oî62lɄ-]wU>Gm ('`7HNa "fKuǶUe]cꫢuUZiShkͨeEac 2Dbk7}T5J0zI ]Rz[3[cbwvQ6 55%SHݑ,S@`H">YG(eY'$~$FK[# z  oA} POA}覤$P?3%00ІJ֒.*G}.3JAӣstrB)!ZOuOc!K@Lc"N3A&*SHiŔ3231P$02fS{*9 ^Zk]Iq(eY,ؤ3N4粸$bTR螅ǑT'ykCem[kJdbX(=)Jփ+DكHYMe[HX3U^6֌вGEy緵6֦jFYB(WX+ijLrڴtBbhV VZMpgGIKƆ)=JGX}(rHAh|ռ%7)A6dݠoMi(' G-4F Atv-2'G郓z'SѲRꨔ*%(ţS )6ompi&2IJJ=94.(` Rn %R S2B(|F-,;Zmta uẢ;j沰BɩTcT,U 9k)P(霅uPԍ.py3RM_ Berl63Hԅ E8k87h= ڿ=?W=岌Rt.w]7/30|~7;a/űks5ܽ.ӈN&Bn|⚷1ׇ>:o$t}f?z6e>[ 7-nf菛ev_obwdG_ aoo?%>EAN|hCnvC]+VmjgirPShKCKJ Lz 9{\ )t 1V~^d 2XNԞ̺'?S)!9o?`R|y,6DQ;(PMu "V+w3H|Qs:vMIdk:_7NA)L5q@qchVԝR UPEPNϯidֵtT%&J¿xM@ !9a&48Lmm#j 2_ቬM?wxL{dA~Ҋ8NG/pѢн'9/Gw?2̔Aj(jÎҤHJʌsly:O]QNL0eCJ J+1 kcKڦdBLQ|%_IXٌEn`C> -J~.\8ҝ$0rVT0Kz"PuC? !IZ $PFHKrA#W ;D=q؋F j<.fȆv0VW+y|9GSPMm; qM>M:m" WF, $*WDitW!gu054S7{S~=hwE4 )Hl8HF /y?gs@9jsĜ9Qds+6Gi#,Ďs8EsJNS$lNrnl@}jsJ?^H~ѡ:eaBuVasN\;biSƳw }E2zm5L 1ħ!tޫ[XKY 0ID ؓʰ*AH3膜&Bίha?j:l$'r'Шw˼b4BQ<jxťPeExIJ%S2r(BI)|+g'\W5QV| ҆)m!( T-2&BQ I@rFE eHĞnݒR4Enō=鄦 ШG sq@ⵁEvf]Y0f \X : QNBN3O ֘B\{[7l ;RiH0MLW;Kă:Y$DJ0YV>"R5"Vd)g/"R,"-θ8Y>i' >O)V|>9)@|#e CL H9j4"Svz*)_ۑ2QM_|OM䥟HMw)VJdzS7P5%I]ק %Ϳe؍5)SN#Gg wXzsXZ47ݨ#  OlR+>z`rfBJRf 2MspvLYuoe",HSqA`0%-ϩBȾ8 %-rV#D|TS .+S()ZR8 I [A'WʼnJ2dwb9Xm4NUoR7XkRϓH=)q% kh;2p>:퉁rΏ x|:y[zYۗ} ˵XB϶|<fe\u~h׀Pm~}7mV \Vfw(|PQH7LQ%nhC 9̳֡j Sh-țx+T[%rh~Q F)|eN9sXV5@J/ORRl^)KNǺb ڶw{5QȰSTJ~(H vO6:\<1&R|)kd#R`A oJ _B8Uc :k١J>R^ +)״vZu[&hƺVj!QT/UUt\ D#)k/\MN9mCzr~?$(|8PajGist.ה")H >lMP8e3a$`zmtS`]E&YXK` ڜf=]fJ#[+rWdBRY-^r+ @=XtʷtG9aZ]$(Qz*+o,36U@:OrtW KGֵ%(S q-}Yv i Q|X}"72^crY:*p4{s"Jw)n.Y`PtA<*W #+|ø;-Sx>`4 q}( K`~֡ J $&D~" aO(jVje%6)qkm\n1H{{vUWXr#E,c$GgI9_(s1H% Ҏ=!|c?zHqhH!z{(--jȺ2(kysPdF8`_\9R)*wĜ=,Ď9*lPhs 1|`Nuq&NR;Z ;POm.T, T&'mSUR#I˔!d>zLr>@` l:߀Pp:L %zc"4 F7%A(ަF>FPd-) ha?.:F,O{[[(=cAfMI4Z'hՊF٭ZN +'?85/FH13_i(%b3Rb0PiaB)KpۂRqj^}6)*#:*rU( {W\(w'ݨk [N('*H%ʹ=p>;c[SvkO 5QXANeq}٭fuOf ~||s6";uX^/"BIi>#N&8m9>j", 棜8-4z3RO-oc0a;I12{Z0PU,uTƝWu3 P&UvgFǝa [i99|z퍙r>`F}9;[N+|m߼z?} ox|xxrNBDDٶ׸߰o،~ M}?'~j6vRyN>wi> ^.Fjm6Ryﮆ,GۖԐbcFDwkcv]; x3ŒޖӮ[3J /I^ں4\캵" 2e_Mn9YSkzY{m,zڰDuO8ݭi.l>L?EqNX:NJRR}f-)xĎ-., n~E{Uk>P" ~ٜ)! _뿵xPm?G `᪴e׭PnvD՛6iaVH9m]KR}jsgUjzEUQl]V^ \y%*k$X[!r$":b$ZQvkqmKU˷2+t%o[&eXn۱؊gc]`4%0e?FYx02=/k6tJbBQ#10şH6g*i`C*˘6@bgS#5:% WJzRz%п[  (f4\:b9=疳Jl#avGvU`%D!;)ĵ3ENHܝ0)0:a]f2JK`~~-gݜЁźp$PZ uaQH9`$P gn 2XN2k(\'vۧq(q#!ҭdﺵN){ƌ[@Dn){D#"eRe$לuk8j(0"G]!(EQKx4 (M* '9 !(I@:+|J$VXvDRP_e,_YzЦ^=({0y[k `ժlw  %nGtg$'kdnQY9x)[C,/H>q֭ueg`oV uku\ó 5q>L޷C6aKV/"B顴כ6aJZ\9`bPq8bjqė8q^ZW(-'KT| pIZ-N 9ax? )*a)=HR?PʼnJ;5q'PFRܭ]܌c3U+_Op*)$'$Hڭu0@KrnVR=ZI-ޭŗm@Zׁx奿F?~~?~;zB=O=B֧mߐx#&>e|=ip9 x|GꍏyeV!Cwx mWܠA |6;Yw q p4S0('/&e L"ԼBXD/}S {iVFdO|2$gkNf]FqW[*Hٻ]BWOMOPCݔyL/⒳%L Hc _i5Crޛ|RgoExc3mȿc{|Ea't }g L/{FEj/Smy~%`wOp3w WPbrix#MڕTL+WVk=H A)g'3kKr,/P|y~ endstream endobj 49 0 obj 8049 endobj 47 0 obj << /Type /Page /Parent 43 0 R /Resources 50 0 R /Contents 48 0 R /MediaBox [0 0 870 290] >> endobj 50 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 52 0 obj << /Length 53 0 R /Filter /FlateDecode >> stream x]M#WnׯڋyVJ%m @6Af$<>ċxnK/=$oi|q~|ijoq1'.Ǐ>~7O_oۛiFʟ2~W~_;|#;/<]c/?w+-f_qZck^:Rb 6xg|/o&|B^!m{7q.:u77_ތ0 o&c a ?I /7 &]} PoHgac4ͧ'֕|!wxܮueYey<2ݧBO/:~mAgp-$ q^My@,wP:X_nX@S'OץI($d}:֞aJ}f>wgiDLn}3Ý+ e6>#PA|3җnrX_GK{xK ?Gxxݙ jކȁ}n&95]Q6ᮅtKeBo+z`PTӷzүšGꙷ kڧ}}xqUR:紏|ks$f><k0' .ߒ ҳ>'oC>i\n:ΏH}q]EL-4M@⇏.` H8M5SỼ\kO\](!G=}~i` L>>ESZ65Gbvp%a`d,M=FwZc)X¾ܓG8 o%S Ð/T+䣞;Ӕ[w$:5O199 ;r)Zcuf0yJ{GK0a]3tSŧ= Rv i@ = _$^Lw -ffo+V0;F3ww{ݣP fdIO|_B=IOZMU;FE)*m,kSX4i}aͳUG@>o.*OmXqݔN "xk/֩g)* DpzOo^_؇нн8WX>,ܱ5Sw-#1<+Ӳ*}fn Zq{MY Ɗ|%Do2j@)&kL"Ƃ2j&&zFÔh /RX|k $eb^_+N>ѨF߇R v ߠ^*(OrSzDDhv1Ѿ#hvq(`> !ۄ5Ez&_'nYZX%kx⑷%(XBI1oXFxB)|r#)(1xIj*`Lm\MҞ ݍ9؏єni$]m!g0{òZܬjn<մGW5!̑{ Qg:$s` -ۆ=AO($e`W/J7Y|'ҹ#(-5oSC߹U61BL̍ +Oqv~Ŕ^D_kIWʍko+t pS5>f2=P8dآ57"SL{(*k})֞ҚQ^ 4Pu\K(8-m J R=L֑>%'/_ S4fiӼq:Ԡy'-dT_ =$ٌAS ,岌4V^*?0}L1F/Ф5 9x]@>maqEk^Oi955xO. \ܧ22{7W242hl_8UWe<^\?hؐz雟G-%Ѡ^>'^o7\ƨ=o@$H$̢?wVνG2 mSՔJZkwAY^##FBH,X YY5_tG 6 SO/.UZ3T v'Tj' $ &oTN>"1?b.\,@RQOH )ԓ|=@rΞ{fov39l˻`J&x.:|;hlfrl1)!1UFP䃞ب=m B(Ҷni'ި.uuh{BGlC.^&UQkFSCk몡P|,=[mE |GiOj@rL2ja6 +TXEW /V+94߰~#;R4q 즑l_'/lHA%"拤:ǩE$(9u,'t5I*|P22 _8QO= [  ARmF_y~&x!J C/J—4qDgF"Z57Z=E)hiKJwyC;N4,v/k]p@7ߐbCηi >;ӽ]zMړzpa7ʧħ)t8qo=G(Q G5ǐ7k3x9B#Od>%63 (N0>+2pj@>܌Es|}6Бi)0^@P2Y/VLp_(l`z0@-K'ԲI ;ZGi5u~LaDٴi ej݂?w@b|ԳLM[df@zz}Iud?ZUhۚ &dsbtNT&jBeLធN`e LaUe) ;n0E_"tcA әBa۠Ng%&KʥAR<ŁP&ރ0ْ6 Q7q=g9jũCX(4ʔ|Mqi@3װQjoiaO8Ebly{Y(lL hry4Kٱho "O@;9 Xq<=}V^,0[}+3XÄsn ,V>E5&_mR[f^c"z,W<6O&5iù FAmXb{v0 |8 ?/?^! 3Bs !`ѣ}f'.l"%|>=` 拔B˃mO$&'Qvɇp 0%`4G=(~|ߏGvJh VhKgZKex(aY# Da @g!)ր]ߗ& H>;0,؏Bf>5C/J qA!a]Spۄ|mI'և9Zqp:n%8ȃ ަn`@tD|ƃn Q]2 ]td+QϸA+j@Q(J0yD?`5`z" .Hi+}[2lpq'BBaHr灧fv06Q)ݜHa^R|0|Mv&-}Q~,1)W+JG4XSҩ``_ )sZ-iM$|Fi*+T(FyK9-qJta ۤg_: E`=OᗤFo z_<Sk5 t*C˕~SW>h~>+(`XDuaSG=p./ S Tӫ;lm0{DHa>k= ;%a|'0_}0@p tM G`~0ݻ<0̷O d?? „Z| Z Bp_(XۃM`@!Zlx HIs2"l>jtos>Nh ϣ|D[ %2PEK{a ,`C˘<,>%ʧГ5PQϒ'wz|5wQam`@̩72B`>7+vd!`[7gll3l lˈ^^iШl K11v*4Z>C#5C[Θ|FLѾӍp_--܅Ԣ4Ef ׃%B.rlQAA5`*X  8¢jN7nטdo~Czݗ\~U]b/o~l?}_oʗA5jmTKC)L㵟&hpV ~7^KoZ\^15yAlipz ~ֽQL~8Пj/3D'p fXZ5=ҷoC!w~o |3r[6 |+ nm>!7Sܮt||cU#l(!8jFcBɅj _Xߏe?7 |۱N{0AV<^뛒 ?D>.YSvW]KJ!P&m㯺UF=׬XP͙hn}d\moqޏhn ucd|FeN4g 欧`֓U? GK HUh96նpTKhmϹ Q`$vY#ECVk@>|i~8g;5 O_o8p{ͤxoXC~,(qu/bn[k>Rp˭pU]( iO C;O8 8k@Swxvy)W_<.SXe?7Ex_|_+OnWyo)t8[KKOSm_3Dc_ ='c}| =\/c}zMMnuC [7ۓp#Ժ)Z >܅ ) =8Z.(50_"p \yld3ԱNJL-G=kʭARЂ<ő8ye;4hWOW?.*u_yC־wѫ;E+Q\ū2n^=F/ßO- dHʠm@>X$R B/o*l ԟLc xr_70!sDvh5E 4 ?YuW\ĈFel">fݥj̈׉!_ؘ2b-+>gOZ*|ϟ;}1`-ћmŷ+ymU3tU)EpTXQ&v@gk(HBF?@;S(&pFQ$- ZĆ%;v |gQ:n(Ab/ J}s;]%:[w ;:@p7hz M`d|=aKGZREEC>e *?_y#_Xg}f+zj}̊wmT.3 B#x> endobj 54 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 56 0 obj << /Length 57 0 R /Filter /FlateDecode >> stream x[M `nGd\J*7G{KrXKQR;vYrRÐ#Z $@x@oL˗alhjPag_Li[qqxϓi=6<}~3|3L $?#fOK| 2٢B,-~A$ڎj;a~~TuH}{|!~ݒWߟund?vQdd̂-4n=ik\x" E"\,t}+H̷VQ͡XL/x`QB: kkyqQNg J9mA i ׏ 1H\?t}gJ"8DPNWcް. <A _%(9 "a-4zӀ`pI-ΞAϒ>30 A Ex@P@*!ZOw 5PRi" 0`YbB(XEf=,Ug€3N" Oܸ1FƦfƦx$Wza}ɟ1hx~y@=$,hR5Աjb˭h8lE~-i^.sp7Gٴh6OOlk?ؑ9ÞS\q spִϻEu;ScLJpKOwm0hG_ |ip4 Xmx_| n7dLHJC zHa^Q/cHs*PD0lXA E-p#O0[I:fI EBcY%W, 1= RMyD ImA^9 +L^ B!)J=iX:#ҀxaAa-cyˇpͅS3$zRlNBOZaAkDI"QaX410aAʔu<@Dl@XOX@* v i֛ bA:KOI?1A@l+KZX4*Hoh"$e|5śn[8(UK%(Ӻ۲0 (Z&LEzSUVAzsxUk iA‚]5 :RLI:%z94,$H  YkX 2S%貖טs˒/az ZGߕD9fr װxOǁMQw8̧`v >4D^$=M_ֈk-/@/@Cy"1r\'Pf;H4}Dg"QF4OD RXv|X'8zQ[=.vt 3W{Sw;:v g‚+&`!OC$ه$ʕRLb6;O=(I_ ޅD/‘u@z~jqPROAIABMIAKOc->EÚBi*>F΂+7zBIr r$_bA?y[gߣԱ`yXEXHO3,SUX )LDX؆x0HO3,X"8H B endstream endobj 57 0 obj 3298 endobj 55 0 obj << /Type /Page /Parent 43 0 R /Resources 58 0 R /Contents 56 0 R /MediaBox [0 0 870 290] >> endobj 58 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 60 0 obj << /Length 61 0 R /Filter /FlateDecode >> stream x[M k @NYxnA,Lv<fA5,+~vܾk?wᅩߗ H_mڷ.vm4N8ۻqӛvÿߴni;ajbl?'3ɏ]`_rEO߿ _lA=Ic(e?.KMկCǥՌ_qǰدߋ dxʧ}ChvlA2S׃b.jn腫yZ?'= ԡ@S>qӭtL40nYaN:eG3(;s;[迍m>&;<$e,GH߸x(PB1Syt]B@]6;)s>6ut[A3) Cy  w,+E6Qڊx86"] ~{lm<4&]Sj^PzRA ϣy)b4H<>sȴ ;ҮנYЧ {b Cb-)D$ N9{(L}A &&|C$B{{v@|OE{R ,(e"b¹cuGVeRG'%7 '9׼xϜk^wh^IzDAR3hC 'ol ͓9)%_\L0nL)|ͣ.)P߲>e~m̿n SZ?y, "D&DiGWvWVЦzRhSWT= m,FV85 J'GL "P EWhzc ُBm*^Dy-ؾSHYYL ڴ6hkKJp3"gL5EPoEt7=T9tJO %ܻ4t=w{)@X,Tϯꅓgp#Lv53%m)3N ]2dXK_!R2ú⋄aI餄zba-<ʰ %RC]5rV4=FWdXb5#y߲鼩 k{::2rP(EaAгdX3µ3iP(aD2h`eUCq/`uUZR YMH9 j@iT=I!U9JR9 YAoH=& kf~ Eԛ8ӅX_6"kPmGJCpjS(J`%]ȒIYGK&_FK6osYBw_aB|RtJE_eI\YCX0 S$9 e/^Sq>u S볍U01 `LF޼+F֩U~caxť1K:\'>T<*^T`>yeڕ--A6o oduuv`HWCB*O@m2 醴H_k[pP)tRɻimBJt&j΢(@*'R"Nzq$ _A"NgPcqRm~cV /V E"NVij?W Dl ^P1>cd{S%]m ̦CcG8CjJOJDY|CAR6&x6T4fgD|fTdy$C`C:&Iv!rJ̃mY#Pǰ5.i/II ȗvjx|OXk=F-BM$ ֩Yؤ2_ pa" '=K4 e]U,i(&ؘ~6WO8 0./:)D[%VWh[ Fj:_.hK4_&mіz&xhc!\іuRzEfo, \NiPB>9N ń6 'RR ),|YU[^ C wפ KA>k;rnw_,`,I4`KjK|OZuJd O-X_6&!`K]IWjB`5XaoxO24eq(ťХP;}do$z3J`!Iς7$Xh':ž>n%-z,`mBD>87ܰ%Σp*= TVv!36{T8}=)}'_5R=v__(=^xː}@"5`nzfM`hƸAo}TY-|eY)ze(C,uwe[Yʄ %VVwRV~&=q@N N=kz왯W&MrYE/ַ* k/PO=*Sv}oٞgE ^CPO唶H"rNRW(np*)P(HO #PFCJރzYF~ˤT 'pL/Q9SHeIRD \Ҁ," ˓fzϷKZoN5 I(\eҢ$>N K \sUR^ROV퓖pfVG7Z͠p^m26>hi <*)#oen7eVpG@Ʊn58O}ˏ`s786,uqev8xDl⃜!?>}SkG_|~3_v endstream endobj 61 0 obj 3835 endobj 59 0 obj << /Type /Page /Parent 43 0 R /Resources 62 0 R /Contents 60 0 R /MediaBox [0 0 1770 470] >> endobj 62 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 64 0 obj << /Length 65 0 R /Filter /FlateDecode >> stream xWˎ1 +̭@o~]Y!$NB  f|<.h4vN%/;2q(.;ЫU84D/y@ʁij~ˮLNO_0QL~Xrq6NMXoHb{| NEjʡm+t15n@o@2`|H<ޞoݗdwH<%Yb"ӞEZjo9k~dqe#v[R(M &L'] 4{AJ(=t2ie@r^ߎ lڗօ8t)w B&lGi~YUyQ ,=E$>LGn+<Î< ODqMD1j oA )Uq[7cygj%esQ.LNlMfQ-#yM֪V"Y*-DՂQ-<=j 6 Y}'E Zɹ'.ъn1U( 5[UOil6 KeV]՟W&d!E(!hm8fpSķƟeXgv_D̺2)坸#jn@{yjGJc(N\4u*M∝碤 &J\P!e(b<{ 8>Q U΁P8wc>Z(=q9rwCs`i=43Z5ΎضYӱ|m7l|~-*Bf8paZ8b ,ε T qYꪯ4\w9gg&:6} wI2ݘtܘq q8_Hy~k_aroǍ۽"2D΅۩_))[pyپ~Ch-n'5 endstream endobj 65 0 obj 999 endobj 63 0 obj << /Type /Page /Parent 43 0 R /Resources 66 0 R /Contents 64 0 R /MediaBox [0 0 1910 498] >> endobj 66 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 68 0 obj << /Length 69 0 R /Filter /FlateDecode >> stream x}WMoFﯘޤCh11P Q ݝ7°E?rf<~/ȐHa-~HGZ_a]Wwh\/<\/"i;Y{*.N']nWJ\S(PL/lϝOqce5--WgFR,[N 1a!:$S qL^ H%xɐJKGd¬8O#mWTOaXapʷz6{[΁Oo{gN>X endstream endobj 69 0 obj 1109 endobj 67 0 obj << /Type /Page /Parent 43 0 R /Resources 70 0 R /Contents 68 0 R /MediaBox [0 0 2120 540] >> endobj 70 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs1 10 0 R /Gs2 11 0 R >> /Font << /TT1 8 0 R /TT2 9 0 R >> >> endobj 3 0 obj << /Type /Pages /Parent 71 0 R /Count 8 /Kids [ 2 0 R 14 0 R 18 0 R 22 0 R 26 0 R 30 0 R 34 0 R 38 0 R ] >> endobj 43 0 obj << /Type /Pages /Parent 71 0 R /Count 7 /Kids [ 42 0 R 47 0 R 51 0 R 55 0 R 59 0 R 63 0 R 67 0 R ] >> endobj 71 0 obj << /Type /Pages /MediaBox [0 0 1770 470] /Count 15 /Kids [ 3 0 R 43 0 R ] >> endobj 72 0 obj << /Type /Catalog /Pages 71 0 R /Version /1.4 >> endobj 8 0 obj << /Type /Font /Subtype /TrueType /BaseFont /BWRTDD+LucidaGrande /FontDescriptor 73 0 R /Encoding /MacRomanEncoding /FirstChar 65 /LastChar 122 /Widths [ 690 0 0 0 0 0 0 0 288 0 0 0 0 0 777 553 777 632 539 632 0 0 0 0 0 0 0 0 0 0 500 0 552 0 512 629 557 368 624 0 289 0 0 289 0 621 614 0 0 409 510 374 621 518 0 613 0 573 ] >> endobj 73 0 obj << /Type /FontDescriptor /FontName /BWRTDD+LucidaGrande /Flags 32 /FontBBox [-1067 -737 1641 1162] /ItalicAngle 0 /Ascent 967 /Descent -211 /CapHeight 723 /StemV 103 /XHeight 530 /StemH 77 /AvgWidth -490 /MaxWidth 1640 /FontFile2 74 0 R >> endobj 74 0 obj << /Length 75 0 R /Length1 15880 /Filter /FlateDecode >> stream x{ |T=fy3, $@4l]X7čZPC]HPh_ϵ;Rն*d7)~̹s9wy7W VvexFg-^HTow`"43=`¥1G%7$ۇ{2V\;&QEF"MC{.]}}"xɒe9_!=|i?"zD}/[:._r~>3fr:b1R:1s_25*9o)82Ե?!$-F}Y~M/s֩kOsJd|-lE-TG8!Lp3yE(^L/FHxK(hKsFvnݫ 'ma:T"tKof{6i}t}x̟[֭\֧ d]{,Yk9l_}ͦx} #XZo/֯H__;.=t$YIge΢,*7;{aH 1 ;32?}Q(qs׈ל_( d6"G݈:~$oLfeV"V_HMf l>KaAI ` _N'"bo_d+:_%8}!@+~o蓽6>2]p,'IdlXv,8$d]Ѽ]$I{7ٓV8B=ANn+ёg eyW"7Y#?Awm"݀{xdgn5=tI(JDɜ\#R%݅/ ;&CQ @j12"W!8WxQ3BhO/iП+Gͩ} 57XQ X No41~IyWx$/ku~]" _)Y1(촪j3zӜfDf7gD4ڍYFC$~>pfY:Hg.sxP7`9!Yqe'J;1Yd!m ЇY1'5'$~1:x6MoBb$z42\4Fo27e7ƣp߿9@=Ҝ0mx$nj}yf \'f4oV57H4yDe[d٥]"77H4Id:6QVhZdҲǗY */S=p農}U׺cgLM/ȭHV- &>(J4ޖ.m+l7GnQ(yh[7ioq <0^d^#\XG6m4z7L,܀F$nA[a26koFn^^_W.f0(S^*.a3S\"0LуمK l܏9Da,"dΊSi w~9^? Cy3~ ɶ?obNoBoA=LspY^g?|Pl4`@ln6RЧ8ttnD/o5^-sЫ(onE+GU@l xehWMsilCUFzAsjvl*ZV߀V Հـ47Aؗ1'4JGRzɇ?"TICI 0 JK<cc)<}l,?2E"cY{n@noi!6#li@qW*otY\/Mbgcc%^.Qtjy,Eg3p[QQ6 q6"#V3zTdlrlX <6Y%3V6ٲ=}{TT͎D $/n!>b:WI&[]E\LV].YdK^B+AUl*asUvn13sN)[и@E$K$ =JD->X6$ڔ@u tcݔ@7'- T@!$Э%9êdˋ@(@chlK*I 4A" c_6mylV,#a$"aeZcgt Dbi2tqʼ{/ ʿӉlP;Fv ;jcL=rGMه vcO~C=fsa?d;JK,sc{^)v56 ??ȸ@9l=[O݇dϩUI6^d Ve'#/ǖg[jyNVn`5⁎BO\2LtU{Yexw糟m#+ >6| Mkhzskj2I%STyր FM;k"69WG6c~ :^6$zEM`.]_N!}9D=~ tI OIJ1i[2tvĻa*k1ymgrX 8HaI: (Ago<WmO3Ʉ5l4@0 ׆Q{YvyW!cleQ,OR1c#dlߋh쫺bO"S4ȿaCmlyֳJ6nV|zKu[JmZ6a `H b|~/vŨ, Dr/T ".'fF 4Kw;&t3u .y?aB'b×,7F7kXFM,-9pCʽhH~}3 fg d}[w9vb6i"@iq%c0`~Src܊Z15c)c^XtxELtriLB)`IL_xRuMPX;U^ncC@o"TeG31%t|<lxXVgK ڍKQcOWc=qd3lb}4#frc[RblvldPR8\R:+ӯ P +{lqqfd=Vrl2ò|GXqG["aq, p1䆒%$o;YC5#F;cڸ)ADpQ,Zc[{6nÊnU^<&A&zΈeN.Y PU]lr%Fe*b4=?v~T 2!$hXi@4- dccC0JC m`cIoCg%6C'Z] WSCz]AKiRjKVl:`uU.;%Ȇ~6,/ @B h/zuo #acc$K:H$`q dVJC,ZU\Q!J @ԫA(lzż=teLV^R,5ON"^g6NcI)$Y2=`2KO|&iH&g~{d\dP =:KZm[66Vc-t{-̰X-5m3ك L8]8OsN#vI3'Uݩr7((SqtsX0m;wbU<ە7W\l0Ex\:P]Xi_=W+Տ}5;?v|Et}'hgXݩGAxCE'UѤCGcB@o}+:XLnQ7|ep>x_| ,t@V|DϟS)|mzc:tE_i#kvlddpta2W tyMȡwsKlۭKbuuU:wvLAq-g&SӃw{/}= .wȷK mx.1Ǽ6yc(QL`Z{&|eG@|tۑ*ORI0 8√ӧt |^6eϫGwMt'^>j[k7-bKlzOW.򲢥l 9y?M4Zg|Qu5֩;Uj.ܲD3Nf _+ NL(Bnw]~x˘SYQw@ сL541 [dWHs /y=WQEn텅<=蓅*W;F҃B>>;oc}Yr\.qz_&h9J>#044@}oc#|+G-nznl02pz[FX&YNJۑ_:ccC0Q~d CP+#HW~YnsVrY;ZaW@3͸"2ä^=mgڰ'W9FG>r–v!6zEe0?I CZj&-CrI}W?'s7*xc뗹{'RR_OϜ[E^m A}(?UI 2ڟb~MQEA 2WTцN>}%P.=>C}[FfGwc: t]͵˥]gwL_?;~tG0Giͫ6-8Vu[MiQI 4U!_b. @&w[ ӞݭzӽMgݰd6ÏFpw򴔾)d',S>*OmD׶=:;=\1{\9fLNރsՆTK1gݵpx[Vf*PR5X<ϲ{ۅvɭv%I@0a'>! J_(5~oTmt* A .~&]zWq!1d`l4^a۹I!]XM0#nQk45;\j5Ɛ eZ̝玟ԾvC{;?_ MöìP~6QS[z:$n^of Jt_ޔ.&Y)úغh^bʾ. bAI"3%39/'򹸥B_1{N#XzF /ڹ5 _AxMfGZt7;;[f7bGeY)ed“W$_r4`tYC=)L*}RiNMQkw㚘]-D)n,iE(^!o nM0WԀ-WV9q-Ir58H]agq8Dp8ApP.G+W(߂Tz$hJK|2^1E^Vbc?Fo*'g5~J]O_l?g|"p7Y#8Ԭ8YM) uR[^ K[ɤ)`,p4- f3k3.zp"jrkÇC8$3cy`n5Fؠ&ΖJnBNr 'ed*]e'bno{Ox IOǧAt%(vDUh4S_/x ?i&a21 3M0=jzkI&@c5ޤed gfQp xMX?C!d<7-'B:cII 5l8`Ucun$VWvgN{vzzd>C0܋-fV&^%+;Mc3q hR \cfG.Y3oQM̗e2PNT&8U:݀pp \-}YtYniC=zsuKuN8I7*I/~QnkuK^--\閾[ڸ}ke.9 endstream endobj 75 0 obj 10024 endobj 9 0 obj << /Type /Font /Subtype /TrueType /BaseFont /THPHEA+LucidaGrande /FontDescriptor 76 0 R /ToUnicode 77 0 R /FirstChar 33 /LastChar 33 /Widths [ 705 ] >> endobj 77 0 obj << /Length 78 0 R /Filter /FlateDecode >> stream x]n <"'4ui!-9䐷vgX^|`Ep$38oM2$;(%{F\X;;dO>}U5o4Bkp8v/&AVԹ2WE(o$.XdC 4ZnZ  2,Թչrr)d#]sM˼z~doM endstream endobj 78 0 obj 228 endobj 76 0 obj << /Type /FontDescriptor /FontName /THPHEA+LucidaGrande /Flags 4 /FontBBox [-1067 -737 1641 1162] /ItalicAngle 0 /Ascent 967 /Descent -211 /CapHeight 723 /StemV 103 /XHeight 530 /StemH 77 /AvgWidth -490 /MaxWidth 1640 /FontFile2 79 0 R >> endobj 79 0 obj << /Length 80 0 R /Length1 9180 /Filter /FlateDecode >> stream xZ tTEU!tH$yG` F!4"? 1sQ\g캣tYdu:Yqf_!L}[_սuoݺuٸaS eQ'I_n[EhkmF5x捲Ed٪!ojꗣ󴶄#%`m68u-F9pWBk[u-F8m}F}mCٟlVӵvSY5/Vf]N/{\?WMs.=LC;%Rq8ݢFRB>9q,grO}_h Xq7sQ}6&YlW;e0{b'O((pٷm.mQ 6ߊbmףi]kM:6oܔ{VhiwrxuKqQ{"VH04 N^.6:n 1$ƲJ__?ZDL"M s [x-08OSL?Oq@nA;>Ijf>'~Gh&~j X}?_}z} 6#D ;^B>`:nv/<8&6Bq0vkc=.T$X],>cS{ߗ/ԇd}X=\}GRG{ HIWJs䤹.hsԕ=/a)QJK5>W) /H5RpBfbՃhb"{dŎX?G!{1x/\S`>Bכ1' OR}/!f~ ՜ Ř ^J 2G֗eqź^}>_+,s)O"{j,ǩ]ԗSlSD\\Yc' )E)6W!e__ y=M2It-X{GϚMOe[M 5d6&ct c@){Ua1_VcP?HJb?9 ._pQHE=8&uN ۓX+`~[?-dĺ\%*jc{L fO_YԯDĐ7"1``[)/Zg੷o?ÑO>wF>g20$*U}]ݷYC}:g58 qm%J].vҭzm;}sv i1ý`鲪ߑl]>SZ(5S棼FV1)RVtt5%4V*!dR&eI(KrCK!#A ?ɟ>净g)A4P<NH@n)(wķ+sa7gs;q%,ݏ<qg Nprg,PH2v.N`9@Y +}={ b^c}a؋At^ ]P/)Z^`+r۫bcgU4 VHZ;6۠Am"h( Zb*xE&2e!+'gNp i̊3 %(T9.u^t^̝8mӜS)NI'OTT{=Neev#ǖ9fMe,i6DF7X!)*-+ϲw%YbeҵҠ,.VU^tʵgbU J8OGz\Bӓ{2.:I'ӓQz_BIG||s/m xg z:+/dGd&ii>Oy=WԎߣ4`*̧"t'~v -xYo9$2ah+ECR=xv}S;y ~Ix2f'1<9\vӃgƽFJ=lMQx׀+1h^=*mf{#xh2͕ďG bbf ZHxnExh/ye#}>J0wd0D+Ōq%.K|1E˨ 98y7i[`7b^0tDlB V ݈&c%V{Ū=\K/k+Ϥi‹Č.zZpr; d&һlfJ2]~Qz`.ZJ-84.iqx3#t@~E|Xw?Z# &c}^)6X*\呺!Cg+ÊOn0&[,q?xYr"o[ݴ&z̽sٌK/xzʹS'OR'^4a|e8-u)p8Yљҭi UP+lhE !|6*Y]J\SdT|6Y \I߻|Ki )m‚[qJɃ07n+7" 3W#@P<~-T\JM܊vn^â^[QCFd{_#Y-!L[ j5X51՘!کbQ|Ոh$t.uQ9 Sruq'2?;:Aih,p fg``Yzm&+曁;QY_p'hBEiD ;'CE5^cAK7WkްFު~ Ф0%ڬm/ NBXF'Z(ڢoHkKH )e4\x$ i9mvIQ_jY4ѽv"IݢpR) |kfN-s#xes wG 7VM;DEBkT@Zק|" U! unHѨO{fn Omց5pcd `4o12@K؛6Yhz:?lhRUӂQd'z ]Qǃ^ 'Z}ZIH*M,hEj׭Rzs9ht"ωxs";hmd}3yK?9BlY_"gw˝F#}Gދ}u|p"9xTpiZMu샕 ŭ1 {oe).#"6^"2GJᖑ#o!!UBz75(Z ZBzӷ *kS :Vt!Hڶa#!7lo"Dx)OEx |UDx_a(q5 TfӆOC$]~b4|3I:?cTk)wN ¯Rםiy-0'$TP%.7=OʷNJ2+d,姗83z2m=h`J)+-qw唕 t(CS*Y[pzKzmϗ֡aԩz֟8= -ǰ)K(\pOTʭ%=t(̓+wuWMϊ;d=w;_tWOrE_/+(}D-GK~Ze>3{+5_W^iH uCruB46a^|nF2 endstream endobj 80 0 obj 4976 endobj 81 0 obj (Mac OS X 10.13.6 Quartz PDFContext) endobj 82 0 obj (D:20181205103730Z00'00') endobj 1 0 obj << /Producer 81 0 R /CreationDate 82 0 R /ModDate 82 0 R >> endobj xref 0 83 0000000000 65535 f 0000141392 00000 n 0000003909 00000 n 0000124330 00000 n 0000000022 00000 n 0000003889 00000 n 0000004014 00000 n 0000005458 00000 n 0000124729 00000 n 0000135464 00000 n 0000004163 00000 n 0000004210 00000 n 0000004255 00000 n 0000005437 00000 n 0000006547 00000 n 0000005494 00000 n 0000006527 00000 n 0000006655 00000 n 0000009925 00000 n 0000006805 00000 n 0000009904 00000 n 0000010032 00000 n 0000017884 00000 n 0000010182 00000 n 0000017863 00000 n 0000017991 00000 n 0000025810 00000 n 0000018141 00000 n 0000025789 00000 n 0000025917 00000 n 0000037452 00000 n 0000026067 00000 n 0000037430 00000 n 0000037559 00000 n 0000061703 00000 n 0000037709 00000 n 0000061681 00000 n 0000061811 00000 n 0000085060 00000 n 0000061961 00000 n 0000085038 00000 n 0000085168 00000 n 0000096820 00000 n 0000124453 00000 n 0000085318 00000 n 0000096798 00000 n 0000096928 00000 n 0000105224 00000 n 0000097078 00000 n 0000105203 00000 n 0000105332 00000 n 0000113409 00000 n 0000105482 00000 n 0000113388 00000 n 0000113517 00000 n 0000117062 00000 n 0000113667 00000 n 0000117041 00000 n 0000117170 00000 n 0000121252 00000 n 0000117320 00000 n 0000121231 00000 n 0000121361 00000 n 0000122606 00000 n 0000121511 00000 n 0000122586 00000 n 0000122715 00000 n 0000124071 00000 n 0000122865 00000 n 0000124050 00000 n 0000124180 00000 n 0000124571 00000 n 0000124664 00000 n 0000125071 00000 n 0000125327 00000 n 0000135442 00000 n 0000135955 00000 n 0000135631 00000 n 0000135935 00000 n 0000136210 00000 n 0000141276 00000 n 0000141297 00000 n 0000141350 00000 n trailer << /Size 83 /Root 72 0 R /Info 1 0 R /ID [ <370c1a3621ef17bd2c9cfaeedad6b57f> <370c1a3621ef17bd2c9cfaeedad6b57f> ] >> startxref 141467 %%EOF booleanOperations-0.9.0/tests/testData/visualTest.py000066400000000000000000000037251356304034400226440ustar00rootroot00000000000000# run in DrawBot RoboFont extension border = 20 dotSize = 10 offDotSize = dotSize * .5 try: CurrentFont except NameError: class CurrentFont(dict): glyphOrder = [] def save(self, path=None): pass try: saveImage except NameError: def saveImage(*args, **kwargs): pass f = CurrentFont() def drawOffCurve(anchor, off): x, y = anchor offx, offy = off if offx or offy: offx += x offy += y with savedState(): stroke(1, 0, 0) fill(1, 0, 0) line((x, y), (offx, offy)) oval(offx - offDotSize, offy - offDotSize, offDotSize * 2, offDotSize * 2) def drawGlyphWithPoints(glyph): fill(0, .1) stroke(0) drawGlyph(glyph) stroke(None) for contour in glyph: fill(0, 1, 0) for point in contour.bPoints: x, y = point.anchor drawOffCurve((x, y), point.bcpIn) drawOffCurve((x, y), point.bcpOut) oval(x - dotSize, y - dotSize, dotSize * 2, dotSize * 2) fill(1, 0, 0) for glyphName in f.glyphOrder: if glyphName not in f: continue g = f[glyphName] bounds = g.bounds if not bounds: continue minx, miny, maxx, maxy = bounds w = maxx - minx h = maxy - miny layerCount = len(f.layers) newPage((w + border) * layerCount + border, h + border * 2 + 100) translate(border, border + 100) translate(-minx, -miny) fontSize(20) stroke() text("%s" % g.name, (w * .5, -100 + miny), align="center") drawGlyphWithPoints(g) translate(w + border, 0) for layer in f.layers: if layer.name == "foreground": continue fill(0) text(layer.name, (w * .5, -100 + miny), align="center") if g.name not in layer: translate(w + border) continue lg = layer[g.name] drawGlyphWithPoints(lg) translate(w + border) saveImage("visualTest.pdf")booleanOperations-0.9.0/tests/test_BooleanGlyph.py000066400000000000000000000055001356304034400223430ustar00rootroot00000000000000from __future__ import print_function, division, absolute_import import sys import os import unittest import pytest from fontPens.digestPointPen import DigestPointPen import defcon import booleanOperations VERBOSE = False class BooleanTests(unittest.TestCase): pass def _makeTestCase(glyph, booleanMethodName, args=None): # get the font font = glyph.font # skip if the booleanMethodName does not exist as layer if booleanMethodName not in font.layers: return False, None expectedLayer = font.layers[booleanMethodName] # skip if the glyph name does not exist in the expected layer if glyph.name not in expectedLayer: return False, None expectedGlyph = expectedLayer[glyph.name] if args is None: if len(glyph) < 2: # skip if not args are given and the glyph has only 1 contour return False, None # set the first contour as subject contour and the rest as clip contour args = [[glyph[0]], glyph[1:]] func = getattr(booleanOperations, booleanMethodName) def test(self): if VERBOSE: print("test: '%s' for '%s'" % (glyph.name, booleanMethodName)) testPen = DigestPointPen() func(*args, outPen=testPen) expectedPen = DigestPointPen() expectedGlyph.drawPoints(expectedPen) self.assertEqual(testPen.getDigest(), expectedPen.getDigest(), "Glyph name '%s' failed for '%s'." % (glyph.name, booleanMethodName)) return True, test def _makeUnionTestCase(glyph, method): return _makeTestCase(glyph, method, args=[glyph]) def _addGlyphTests(): root = os.path.join(os.path.dirname(__file__), 'testData') path = os.path.join(root, "test.ufo") font = defcon.Font(path) booleanMethods = { "union": _makeUnionTestCase, "difference": _makeTestCase, "intersection": _makeTestCase, "xor": _makeTestCase, } for glyph in font: for booleanMethod, testMaker in booleanMethods.items(): shouldPerformTest, testMethod = testMaker(glyph, booleanMethod) if shouldPerformTest: testMethodName = "test_%s_%s" % (glyph.name, booleanMethod) testMethod.__name__ = str(testMethodName) setattr(BooleanTests, testMethodName, testMethod) _addGlyphTests() def test_unsupported_qcurve(): font = defcon.Font() g = font.newGlyph("test") p = g.getPointPen() p.beginPath() p.addPoint((0, 0), segmentType="line") p.addPoint((100, 0), segmentType="line") p.addPoint((100, 100), segmentType="line") p.addPoint((50, 100)) p.addPoint((0, 100), segmentType="qcurve") p.endPath() with pytest.raises(booleanOperations.exceptions.UnsupportedContourError): booleanOperations.union(g, None) if __name__ == '__main__': sys.exit(unittest.main()) booleanOperations-0.9.0/tox.ini000066400000000000000000000003351356304034400165210ustar00rootroot00000000000000[tox] envlist = py{36,37} [testenv] deps = defcon fontPens pytest -rrequirements.txt download = True commands = # pass to pytest any extra positional arguments after `tox -- ...` pytest {posargs}