def _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path: # skia-pathops has a bug where it sometimes fails to simplify paths when there # are float coordinates and control points are very close to one another. # Rounding coordinates to integers works around the bug. # Since we are going to round glyf coordinates later on anyway, here it is # ok(-ish) to also round before simplify. Better than failing the whole process # for the entire font. # https://bugs.chromium.org/p/skia/issues/detail?id=11958 # https://github.com/google/fonts/issues/3365 # TODO(anthrotype): remove once this Skia bug is fixed try: return pathops.simplify(path, clockwise=path.clockwise) except pathops.PathOpsError: pass path = _round_path(path) try: path = pathops.simplify(path, clockwise=path.clockwise) log.debug( "skia-pathops failed to simplify '%s' with float coordinates, " "but succeded using rounded integer coordinates", debugGlyphName, ) return path except pathops.PathOpsError as e: path.dump() raise RemoveOverlapsError( f"Failed to remove overlaps from glyph {debugGlyphName!r}") from e raise AssertionError("Unreachable")
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 __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 ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph: # Skia paths have no 'components', no need for glyphSet ttPen = TTGlyphPen(glyphSet=None) path.draw(ttPen) glyph = ttPen.glyph() assert not glyph.isComposite() # compute glyph.xMin (glyfTable parameter unused for non composites) glyph.recalcBounds(glyfTable=None) return glyph
def skia_path_to_ttfont_glyph(skia_path: pathops.Path) -> _g_l_y_f.Glyph: """ Converts a pathops.Path object to a fontTools.ttLib._g_l_y_f.Glyph object """ tt_pen = TTGlyphPen(glyphSet=None) skia_path.draw(tt_pen) glyph = tt_pen.glyph() glyph.recalcBounds(glyfTable=None) return glyph
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 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_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_eq_operator(self): path1 = Path() path2 = Path() assert path1 == path2 path1.moveTo(0, 0) assert path1 != path2 path2.moveTo(0, 0) assert path1 == path2 path1.fillType = FillType.EVEN_ODD assert path1 != path2
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_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 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 test_path_operation(message, operations, expected): path = Path() for op, args in operations: getattr(path, op)(*args) # round the values we get back rounded = [] for verb, pts in path.segments: round_pts = [] for pt in pts: round_pts.append(tuple(round(c, 2) for c in pt)) rounded.append((verb, tuple(round_pts))) assert tuple(rounded) == expected, message
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 __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_reverse_path(operations, expected): path = Path() for verb, pts in operations: path.add(verb, *pts) path.reverse() assert list(path) == expected
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 _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 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))
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 test_init(self): path = Path() assert isinstance(path, Path)
def build(instance, isTTF, version): font = instance.parent source = font.masters[0] fea, marks = makeFeatures(instance, source) source.blueValues = [] source.otherBlues = [] for zone in sorted(source.alignmentZones): pos = zone.position size = zone.size vals = sorted((pos, pos + size)) if pos == 0 or size >= 0: source.blueValues.extend(vals) else: source.otherBlues.extend(vals) fontinfo = f""" FontName {instance.fontName} OrigEmSqUnits {font.upm} DominantV {source.verticalStems} DominantH {source.horizontalStems} BaselineOvershoot {source.blueValues[0]} BaselineYCoord {source.blueValues[1]} LcHeight {source.blueValues[2]} LcOvershoot {source.blueValues[3] - source.blueValues[2]} CapHeight {source.blueValues[4]} CapOvershoot {source.blueValues[5] - source.blueValues[4]} AscenderHeight {source.blueValues[6]} AscenderOvershoot {source.blueValues[7] - source.blueValues[6]} Baseline5 {source.otherBlues[1]} Baseline5Overshoot {source.otherBlues[0] - source.otherBlues[1]} FlexOK true BlueFuzz 1 """ characterMap = {} glyphs = {} metrics = {} layerSet = {g.name: g.layers[source.id] for g in font.glyphs} if isTTF: from fontTools.pens.cu2quPen import Cu2QuPen from fontTools.pens.recordingPen import RecordingPen for glyph in font.glyphs: layer = glyph.layers[source.id] pen = RecordingPen() layer.draw(pen) layer.paths = [] layer.components = [] pen.replay(Cu2QuPen(layer.getPen(), 1.0, reverse_direction=True)) for glyph in font.glyphs: if not glyph.export and not isTTF: continue name = glyph.name for code in glyph.unicodes: characterMap[int(code, 16)] = name layer = glyph.layers[source.id] width = 0 if name in marks else layer.width pen = BoundsPen(layerSet) layer.draw(pen) metrics[name] = (width, pen.bounds[0] if pen.bounds else 0) if isTTF: from fontTools.pens.ttGlyphPen import TTGlyphPen pen = TTGlyphPen(layerSet) if layer.paths: # Decompose and remove overlaps. path = Path() layer.draw(DecomposePathPen(path, layerSet)) path.simplify(fix_winding=True, keep_starting_points=True) path.draw(pen) else: # Composite-only glyph, no need to decompose. layer.draw(FlattenComponentsPen(pen, layerSet)) glyphs[name] = pen.glyph() else: from fontTools.pens.t2CharStringPen import T2CharStringPen # Draw glyph and remove overlaps. path = Path() layer.draw(DecomposePathPen(path, layerSet)) path.simplify(fix_winding=True, keep_starting_points=True) # Build CharString. pen = T2CharStringPen(width, None) path.draw(pen) glyphs[name] = pen.getCharString() vendor = font.customParameters["vendorID"] names = { "copyright": font.copyright, "familyName": instance.familyName, "styleName": instance.name, "uniqueFontIdentifier": f"{version};{vendor};{instance.fontName}", "fullName": instance.fullName, "version": f"Version {version}", "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=isTTF) fb.updateHead(fontRevision=version, created=date, modified=date) fb.setupGlyphOrder(font.glyphOrder) fb.setupCharacterMap(characterMap) fb.setupNameTable(names, mac=False) fb.setupHorizontalHeader( ascent=source.ascender, descent=source.descender, lineGap=source.customParameters["typoLineGap"], ) if isTTF: fb.setupGlyf(glyphs) else: privateDict = { "BlueValues": source.blueValues, "OtherBlues": source.otherBlues, "StemSnapH": source.horizontalStems, "StemSnapV": source.verticalStems, "StdHW": source.horizontalStems[0], "StdVW": source.verticalStems[0], } fontInfo = { "FullName": names["fullName"], "Notice": names["copyright"].replace("©", "\(c\)"), "version": f"{version}", "Weight": instance.name, } fb.setupCFF(names["psName"], fontInfo, glyphs, privateDict) fb.setupHorizontalMetrics(metrics) codePages = [CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]] fb.setupOS2( version=4, sTypoAscender=source.ascender, sTypoDescender=source.descender, sTypoLineGap=source.customParameters["typoLineGap"], usWinAscent=source.ascender, usWinDescent=-source.descender, sxHeight=source.xHeight, sCapHeight=source.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), ) underlineThickness = int(source.customParameters["underlineThickness"]) underlinePosition = int(source.customParameters["underlinePosition"]) fb.setupPost( keepGlyphNames=False, underlineThickness=underlineThickness, underlinePosition=underlinePosition + underlineThickness // 2, ) fb.font["meta"] = meta = newTable("meta") meta.data = {"dlng": "Arab", "slng": "Arab"} fb.addOpenTypeFeatures(fea) if isTTF: from fontTools.ttLib.tables import ttProgram fb.setupDummyDSIG() fb.font["gasp"] = gasp = newTable("gasp") gasp.gaspRange = {0xFFFF: 15} fb.font["prep"] = prep = newTable("prep") prep.program = ttProgram.Program() assembly = ["PUSHW[]", "511", "SCANCTRL[]", "PUSHB[]", "4", "SCANTYPE[]"] prep.program.fromAssembly(assembly) else: from cffsubr import subroutinize subroutinize(fb.font) return fb.font
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_duplicate_start_point(): # https://github.com/fonttools/skia-pathops/issues/13 path = Path() path.moveTo( bits2float(0x43480000), # 200 bits2float(0x43db8ce9), # 439.101 ) path.lineTo( bits2float(0x43480000), # 200 bits2float(0x4401c000), # 519 ) path.cubicTo( bits2float(0x43480000), # 200 bits2float(0x441f0000), # 636 bits2float(0x43660000), # 230 bits2float(0x44340000), # 720 bits2float(0x43c80000), # 400 bits2float(0x44340000), # 720 ) path.cubicTo( bits2float(0x4404c000), # 531 bits2float(0x44340000), # 720 bits2float(0x440d0000), # 564 bits2float(0x442b8000), # 686 bits2float(0x44118000), # 582 bits2float(0x4416c000), # 603 ) path.lineTo( bits2float(0x442cc000), # 691 bits2float(0x441c8000), # 626 ) path.cubicTo( bits2float(0x44260000), # 664 bits2float(0x443d4000), # 757 bits2float(0x44114000), # 581 bits2float(0x444a8000), # 810 bits2float(0x43c88000), # 401 bits2float(0x444a8000), # 810 ) path.cubicTo( bits2float(0x43350000), # 181 bits2float(0x444a8000), # 810 bits2float(0x42c80000), # 100 bits2float(0x442e0000), # 696 bits2float(0x42c80000), # 100 bits2float(0x4401c000), # 519 ) path.lineTo( bits2float(0x42c80000), # 100 bits2float(0x438a8000), # 277 ) path.cubicTo( bits2float(0x42c80000), # 100 bits2float(0x42cc0000), # 102 bits2float(0x433e0000), # 190 bits2float(0xc1200000), # -10 bits2float(0x43cd0000), # 410 bits2float(0xc1200000), # -10 ) path.cubicTo( bits2float(0x441d8000), # 630 bits2float(0xc1200000), # -10 bits2float(0x442f0000), # 700 bits2float(0x42e60000), # 115 bits2float(0x442f0000), # 700 bits2float(0x437a0000), # 250 ) path.lineTo( bits2float(0x442f0000), # 700 bits2float(0x43880000), # 272 ) path.cubicTo( bits2float(0x442f0000), # 700 bits2float(0x43d18000), # 419 bits2float(0x44164000), # 601 bits2float(0x43fa0000), # 500 bits2float(0x43c88000), # 401 bits2float(0x43fa0000), # 500 ) path.cubicTo( bits2float(0x43964752), # 300.557 bits2float(0x43fa0000), # 500 bits2float(0x436db1ed), # 237.695 bits2float(0x43ef6824), # 478.814 bits2float(0x43480000), # 200 bits2float(0x43db8ce9), # 439.101 ) path.close() path.moveTo( bits2float(0x434805cb), # 200.023 bits2float(0x43881798), # 272.184 ) path.cubicTo( bits2float(0x43493da4), # 201.241 bits2float(0x43b2a869), # 357.316 bits2float(0x437bd6b1), # 251.839 bits2float(0x43cd0000), # 410 bits2float(0x43c80000), # 400 bits2float(0x43cd0000), # 410 ) path.cubicTo( bits2float(0x44098000), # 550 bits2float(0x43cd0000), # 410 bits2float(0x44160000), # 600 bits2float(0x43b20000), # 356 bits2float(0x44160000), # 600 bits2float(0x43868000), # 269 ) path.lineTo( bits2float(0x44160000), # 600 bits2float(0x43808000), # 257 ) path.cubicTo( bits2float(0x44160000), # 600 bits2float(0x43330000), # 179 bits2float(0x44110000), # 580 bits2float(0x429c0000), # 78 bits2float(0x43cd0000), # 410 bits2float(0x429c0000), # 78 ) path.cubicTo( bits2float(0x43725298), # 242.323 bits2float(0x429c0000), # 78 bits2float(0x43491e05), # 201.117 bits2float(0x431ccd43), # 156.802 bits2float(0x434805cb), # 200.023 bits2float(0x43881797), # 272.184 ) path.close() contours = list(path.contours) # on the second contour, the last and first points' Y coordinate only # differ by one bit: 0x43881798 != 0x43881797 points = contours[1].points assert points[0] != points[-1] assert points[0] == pytest.approx(points[-1]) # when "drawn" as segments, almost equal last/first points are treated # as exactly equal, without the need of an extra closing lineTo for contour in path.contours: segments = list(contour.segments) assert segments[-1][0] == "closePath" first_type, first_pts = segments[0] last_type, last_pts = segments[-2] assert first_type == "moveTo" assert last_type == "curveTo" assert last_pts[-1] == first_pts[-1]
def test_transform(self): path = Path() path.moveTo(125, 376) path.cubicTo(181, 376, 218, 339, 218, 290) path.cubicTo(218, 225, 179, 206, 125, 206) path.close() # t = Transform().rotate(radians(-45)).translate(-100, 0) matrix = (0.707107, -0.707107, 0.707107, 0.707107, -70.7107, 70.7107) result = path.transform(*matrix) expected = Path() expected.moveTo( bits2float(0x438dc663), # 283.55 bits2float(0x437831ce), # 248.195 ) expected.cubicTo( bits2float(0x43a192ee), # 323.148 bits2float(0x435098b8), # 208.597 bits2float(0x43a192ee), # 323.148 bits2float(0x431c454a), # 156.271 bits2float(0x43903ff5), # 288.5 bits2float(0x42f33ead), # 121.622 ) expected.cubicTo( bits2float(0x437289a8), # 242.538 bits2float(0x42975227), # 75.6605 bits2float(0x43498688), # 201.526 bits2float(0x42b39aee), # 89.8026 bits2float(0x4323577c), # 163.342 bits2float(0x42fff906), # 127.986 ) expected.close() result.dump(as_hex=True) assert result == expected
def overlapping_path(): path = Path() path.moveTo(0, 0) path.lineTo(10, 0) path.lineTo(10, 10) path.lineTo(0, 10) path.close() path.moveTo(5, 5) path.lineTo(15, 5) path.lineTo(15, 15) path.lineTo(5, 15) path.close() return path
def test_copy(self): path1 = Path() path2 = Path(path1) assert path1 == path2
def _convert_vmobject_to_skia_path(self, vmobject: VMobject) -> SkiaPath: """Converts a :class:`~.VMobject` to SkiaPath. This method only works for cairo renderer because it treats the points as Cubic beizer curves. Parameters ---------- vmobject: The :class:`~.VMobject` to convert from. Returns ------- SkiaPath: The converted path. """ path = SkiaPath() if not np.all(np.isfinite(vmobject.points)): points = np.zeros((1, 3)) # point invalid? else: points = vmobject.points if len(points) == 0: # what? No points so return empty path return path # In OpenGL it's quadratic beizer curves while on Cairo it's cubic... if config.renderer == "opengl": subpaths = vmobject.get_subpaths_from_points(points) for subpath in subpaths: quads = vmobject.get_bezier_tuples_from_points(subpath) start = subpath[0] path.moveTo(*start[:2]) for p0, p1, p2 in quads: path.quadTo(*p1[:2], *p2[:2]) if vmobject.consider_points_equals(subpath[0], subpath[-1]): path.close() else: subpaths = vmobject.gen_subpaths_from_points_2d(points) for subpath in subpaths: quads = vmobject.gen_cubic_bezier_tuples_from_points(subpath) start = subpath[0] path.moveTo(*start[:2]) for p0, p1, p2, p3 in quads: path.cubicTo(*p1[:2], *p2[:2], *p3[:2]) if vmobject.consider_points_equals_2d(subpath[0], subpath[-1]): path.close() return path
def test_strip_collinear_moveTo(): # https://github.com/fonttools/skia-pathops/issues/12 path = Path() path.moveTo( bits2float(0x440b8000), # 558 bits2float(0x0), # 0 ) path.lineTo( bits2float(0x44098000), # 550 bits2float(0x0), # 0 ) path.lineTo( bits2float(0x440c247f), # 560.57 bits2float(0x41daf87e), # 27.3713 ) path.lineTo( bits2float(0x440e247f), # 568.57 bits2float(0x41daf87e), # 27.3713 ) path.close() path.moveTo( bits2float(0x440b0000), # 556 bits2float(0x40e00000), # 7 ) path.lineTo( bits2float(0x440a4000), # 553 bits2float(0x0), # 0 ) path.lineTo( bits2float(0x44049c26), # 530.44 bits2float(0x0), # 0 ) path.lineTo( bits2float(0x44052891), # 532.634 bits2float(0x40e00000), # 7 ) path.close() path.simplify() expected = Path() expected.moveTo( bits2float(0x440b8000), # 558 bits2float(0x0), # 0 ) expected.lineTo( bits2float(0x440e247f), # 568.57 bits2float(0x41daf87e), # 27.3713 ) expected.lineTo( bits2float(0x440c247f), # 560.57 bits2float(0x41daf87e), # 27.3713 ) expected.lineTo( bits2float(0x440a2d02), # 552.703 bits2float(0x40e00000), # 7 ) expected.lineTo( bits2float(0x44052891), # 532.634 bits2float(0x40e00000), # 7 ) expected.lineTo( bits2float(0x44049c26), # 530.44 bits2float(0x0), # 0 ) # expected.lineTo( # bits2float(0x44098000), # 550 # bits2float(0x0), # 0 # ) expected.close() assert list(path) == list(expected)