def calculate_pathop(pen1, pen2, operation): if USE_SKIA_PATHOPS: p1 = Path() pen1.replay(p1.getPen()) if operation == BooleanOp.Simplify: # ignore pen2 p1.simplify(fix_winding=True, keep_starting_points=True) d0 = RecordingPen() p1.draw(d0) return d0.value if pen2: p2 = Path() pen2.replay(p2.getPen()) builder = OpBuilder(fix_winding=True, keep_starting_points=True) builder.add(p1, PathOp.UNION) if pen2: builder.add(p2, BooleanOp.Skia(operation)) result = builder.resolve() d0 = RecordingPen() result.draw(d0) return d0.value else: bg2 = BooleanGlyph() if pen2: pen2.replay(bg2.getPen()) bg = BooleanGlyph() pen1.replay(bg.getPen()) bg = bg._booleanMath(BooleanOp.BooleanGlyphMethod(operation), bg2) dp = RecordingPen() bg.draw(dp) return dp.value
def test_resolve(self): path1 = Path() pen1 = path1.getPen() pen1.moveTo((5, -225)) pen1.lineTo((-225, 7425)) pen1.lineTo((7425, 7425)) pen1.lineTo((7425, -225)) pen1.lineTo((-225, -225)) pen1.closePath() path2 = Path() pen2 = path2.getPen() pen2.moveTo((5940, 2790)) pen2.lineTo((5940, 2160)) pen2.lineTo((5970, 1980)) pen2.lineTo((5688, 773669888)) pen2.lineTo((5688, 2160)) pen2.lineTo((5688, 2430)) pen2.lineTo((5400, 4590)) pen2.lineTo((5220, 4590)) pen2.lineTo((5220, 4920)) pen2.curveTo((5182.22900390625, 4948.328125), (5160, 4992.78662109375), (5160, 5040.00048828125)) pen2.lineTo((5940, 2790)) pen2.closePath() builder = OpBuilder(fix_winding=False, keep_starting_points=False) builder.add(path1, PathOp.UNION) builder.add(path2, PathOp.UNION) result = builder.resolve() assert list(result.segments) == [ ("moveTo", ((5316.0, 4590.0),)), ("lineTo", ((5220.0, 4590.0),)), ("lineTo", ((5220.0, 4866.92333984375),)), ("lineTo", ((5316.0, 4590.0),)), ("closePath", ()), ("moveTo", ((5192.18701171875, 4947.15283203125),)), ( "curveTo", ( (5171.5654296875, 4973.322265625), (5160.0, 5005.9443359375), (5160.0, 5040.00048828125), ), ), ("lineTo", ((5192.18701171875, 4947.15283203125),)), ("closePath", ()), ("moveTo", ((5688.0, 7425.0),)), ("lineTo", ((-225.0, 7425.0),)), ("lineTo", ((5.0, -225.0),)), ("lineTo", ((7425.0, -225.0),)), ("lineTo", ((7425.0, 7425.0),)), ("lineTo", ((5688.0, 7425.0),)), ("closePath", ()), ]
def test_draw(self): path = Path() pen = path.getPen() pen.moveTo((0, 0)) pen.lineTo((1.0, 2.0)) pen.curveTo((3.5, 4), (5, 6), (7, 8)) pen.qCurveTo((9, 10), (11, 12)) pen.closePath() path2 = Path() path.draw(path2.getPen()) assert path == path2
def _doPathOp(self, other, operator): from pathops import Path, op path1 = Path() path2 = Path() self.drawToPen(path1.getPen()) other.drawToPen(path2.getPen()) result = op( path1, path2, operator, fix_winding=True, keep_starting_points=True, ) resultPath = BezierPath() result.draw(resultPath) return resultPath
def __init__(self, subject, clip, **kwargs) -> None: super().__init__(**kwargs) outpen = SkiaPath() xor( [self._convert_vmobject_to_skia_path(subject)], [self._convert_vmobject_to_skia_path(clip)], outpen.getPen(), ) self._convert_skia_path_to_vmobject(outpen)
def __init__(self, *vmobjects: VMobject, **kwargs) -> None: if len(vmobjects) < 2: raise ValueError("At least 2 mobjects needed for Union.") super().__init__(**kwargs) paths = [] for vmobject in vmobjects: paths.append(self._convert_vmobject_to_skia_path(vmobject)) outpen = SkiaPath() union(paths, outpen.getPen()) self._convert_skia_path_to_vmobject(outpen)
def removeOverlap(self): from pathops import Path path = Path() self.drawToPen(path.getPen()) path.simplify( fix_winding=True, keep_starting_points=False, ) resultPath = BezierPath() path.draw(resultPath) self.path = resultPath.path
def test_last_implicit_lineTo(self): # https://github.com/fonttools/skia-pathops/issues/6 path = Path() pen = path.getPen() pen.moveTo((100, 100)) pen.lineTo((100, 200)) pen.closePath() assert list(path.segments) == [ ('moveTo', ((100.0, 100.0),)), ('lineTo', ((100.0, 200.0),)), # ('lineTo', ((100.0, 100.0),)), ('closePath', ())]
def test_intersection(subject_path, clip_path, expected): sub = Path() for verb, pts in subject_path: sub.add(verb, *pts) clip = Path() for verb, pts in clip_path: clip.add(verb, *pts) result = Path() intersection([sub], [clip], result.getPen()) assert list(result) == expected
def test_add(self): path = Path() pen = path.getPen() pen.moveTo((5, -225)) pen.lineTo((-225, 7425)) pen.lineTo((7425, 7425)) pen.lineTo((7425, -225)) pen.lineTo((-225, -225)) pen.closePath() builder = OpBuilder() builder.add(path, PathOp.UNION)
def test_pen_addComponent_decomposed_from_glyphSet(self): a = Path() a.moveTo(0, 0) a.lineTo(1, 0) a.lineTo(1, 1) a.lineTo(0, 1) a.close() glyphSet = {"a": a} b = Path() pen = b.getPen(glyphSet=glyphSet) pen.addComponent("a", (2, 0, 0, 2, 10, 10)) glyphSet["b"] = b assert list(b) == [ (PathVerb.MOVE, ((10, 10),)), (PathVerb.LINE, ((12, 10),)), (PathVerb.LINE, ((12, 12),)), (PathVerb.LINE, ((10, 12),)), (PathVerb.CLOSE, ()), ] c = Path() pen = c.getPen(glyphSet=glyphSet) pen.addComponent("a", (1, 0, 0, 1, 2, 2)) pen.addComponent("b", (1, 0, 0, 1, -10, -10)) glyphSet["c"] = c assert list(c) == [ (PathVerb.MOVE, ((2, 2),)), (PathVerb.LINE, ((3, 2),)), (PathVerb.LINE, ((3, 3),)), (PathVerb.LINE, ((2, 3),)), (PathVerb.CLOSE, ()), (PathVerb.MOVE, ((0, 0),)), (PathVerb.LINE, ((2, 0),)), (PathVerb.LINE, ((2, 2),)), (PathVerb.LINE, ((0, 2),)), (PathVerb.CLOSE, ()), ]
def __init__(self, *vmobjects, **kwargs) -> None: if len(vmobjects) < 2: raise ValueError("At least 2 mobjects needed for Intersection.") super().__init__(**kwargs) outpen = SkiaPath() intersection( [self._convert_vmobject_to_skia_path(vmobjects[0])], [self._convert_vmobject_to_skia_path(vmobjects[1])], outpen.getPen(), ) new_outpen = outpen for _i in range(2, len(vmobjects)): new_outpen = SkiaPath() intersection( [outpen], [self._convert_vmobject_to_skia_path(vmobjects[_i])], new_outpen.getPen(), ) outpen = new_outpen self._convert_skia_path_to_vmobject(outpen)
def test_allow_open_contour(self): path = Path() pen = path.getPen() pen.moveTo((0, 0)) # pen.endPath() is implicit here pen.moveTo((1, 0)) pen.lineTo((1, 1)) pen.curveTo((2, 2), (3, 3), (4, 4)) pen.endPath() assert list(path.segments) == [ ('moveTo', ((0.0, 0.0),)), ('endPath', ()), ('moveTo', ((1.0, 0.0),)), ('lineTo', ((1.0, 1.0),)), ('curveTo', ((2.0, 2.0), (3.0, 3.0), (4.0, 4.0))), ('endPath', ()), ]
def test_decompose_join_quadratic_segments(self): path = Path() pen = path.getPen() pen.moveTo((0, 0)) pen.qCurveTo((1, 1), (2, 2), (3, 3)) pen.closePath() items = list(path) assert len(items) == 4 # the TrueType quadratic spline with N off-curves is stored internally # as N atomic quadratic Bezier segments assert items[1][0] == PathVerb.QUAD assert items[1][1] == ((1.0, 1.0), (1.5, 1.5)) assert items[2][0] == PathVerb.QUAD assert items[2][1] == ((2.0, 2.0), (3.0, 3.0)) # when drawn back onto a SegmentPen, the implicit on-curves are omitted assert list(path.segments) == [ ('moveTo', ((0.0, 0.0),)), ('qCurveTo', ((1.0, 1.0), (2.0, 2.0), (3.0, 3.0))), ('closePath', ())]
def build(instance, opts): font = instance.parent master = font.masters[0] fea, marks = makeFeatures(instance, master) glyphOrder = [] advanceWidths = {} characterMap = {} charStrings = {} for glyph in font.glyphs: if not glyph.export: continue name = glyph.name glyphOrder.append(name) if glyph.unicode: characterMap[int(glyph.unicode, 16)] = name layer = getLayer(glyph, instance) width = 0 if name in marks else layer.width path = Path() draw(layer, instance, path.getPen()) path.simplify(fix_winding=True, keep_starting_points=True) pen = T2CharStringPen(width, None) path.draw(pen) charStrings[name] = pen.getCharString(optimize=False) advanceWidths[name] = width # XXX glyphOrder.pop(glyphOrder.index(".notdef")) glyphOrder.pop(glyphOrder.index("space")) glyphOrder.insert(0, ".notdef") glyphOrder.insert(1, "space") version = float(opts.version) vendor = font.customParameters["vendorID"] names = { "copyright": font.copyright, "familyName": instance.familyName, "styleName": instance.name, "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}", "fullName": instance.fullName, "version": f"Version {version:.03f}", "psName": instance.fontName, "manufacturer": font.manufacturer, "designer": font.designer, "description": font.customParameters["description"], "vendorURL": font.manufacturerURL, "designerURL": font.designerURL, "licenseDescription": font.customParameters["license"], "licenseInfoURL": font.customParameters["licenseURL"], "sampleText": font.customParameters["sampleText"], } date = int(font.date.timestamp()) - epoch_diff fb = FontBuilder(font.upm, isTTF=False) fb.updateHead(fontRevision=version, created=date, modified=date) fb.setupGlyphOrder(glyphOrder) fb.setupCharacterMap(characterMap) fb.setupNameTable(names, mac=False) fb.setupHorizontalHeader(ascent=master.ascender, descent=master.descender, lineGap=master.customParameters["typoLineGap"]) privateDict = { "BlueValues": [], "OtherBlues": [], "StemSnapH": master.horizontalStems, "StemSnapV": master.verticalStems, "StdHW": master.horizontalStems[0], "StdVW": master.verticalStems[0], } for zone in sorted(master.alignmentZones): pos = zone.position size = zone.size vals = privateDict["BlueValues"] if pos == 0 or size >= 0 else privateDict["OtherBlues"] vals.extend(sorted((pos, pos + size))) fontInfo = { "FullName": names["fullName"], "Notice": names["copyright"].replace("©", "\(c\)"), "version": f"{version:07.03f}", "Weight": instance.name, } fb.setupCFF(names["psName"], fontInfo, charStrings, privateDict) metrics = {} for i, (name, width) in enumerate(advanceWidths.items()): bounds = charStrings[name].calcBounds(None) or [0] metrics[name] = (width, bounds[0]) fb.setupHorizontalMetrics(metrics) codePages = [CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]] fb.setupOS2(version=4, sTypoAscender=master.ascender, sTypoDescender=master.descender, sTypoLineGap=master.customParameters["typoLineGap"], usWinAscent=master.ascender, usWinDescent=-master.descender, sxHeight=master.xHeight, sCapHeight=master.capHeight, achVendID=vendor, fsType=calcBits(font.customParameters["fsType"], 0, 16), fsSelection=calcFsSelection(instance), ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0, 32), ulCodePageRange1=calcBits(codePages, 0, 32)) ut = int(master.customParameters["underlineThickness"]) up = int(master.customParameters["underlinePosition"]) fb.setupPost(underlineThickness=ut, underlinePosition=up + ut//2) fb.addOpenTypeFeatures(fea) cidinfo = f""" FontName ({names["psName"]}) FamilyName ({names["familyName"]}) Weight ({fontInfo["Weight"]}) version ({fontInfo["version"]}) Notice ({fontInfo["Notice"]}) Registry Adobe Ordering Identity Supplement 0 """ cidmap = f"mergefonts {instance.fontName}\n" \ + "\n".join([f"{i} {n}" for i, n in enumerate(glyphOrder)]) return fb.font, cidinfo, cidmap
def test_raise_open_contour_error(self): path = Path() pen = path.getPen(allow_open_paths=False) pen.moveTo((0, 0)) with pytest.raises(OpenPathError): pen.endPath()
def test_getPen(self): path = Path() pen = path.getPen() assert isinstance(pen, PathPen) assert id(pen) != id(path.getPen())
def test_pen_addComponent_missing_required_glyphSet(self): path = Path() pen = path.getPen() with pytest.raises(TypeError, match="Missing required glyphSet"): pen.addComponent("a", (1, 0, 0, 1, 0, 0))