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")
Example #2
0
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
Example #3
0
 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
Example #5
0
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
Example #6
0
 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)
Example #7
0
 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', ())]
Example #8
0
    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)
Example #9
0
 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
Example #10
0
    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', ()),
        ]
Example #11
0
    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
Example #12
0
    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', ())]
Example #13
0
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
Example #14
0
 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
Example #15
0
    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)
Example #16
0
def test_reverse_path(operations, expected):
    path = Path()
    for verb, pts in operations:
        path.add(verb, *pts)

    path.reverse()

    assert list(path) == expected
Example #17
0
    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", ()),
        ]
Example #18
0
 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
Example #19
0
 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))
Example #20
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, ()),
        ]
Example #21
0
 def test_init(self):
     path = Path()
     assert isinstance(path, Path)
Example #22
0
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
Example #23
0
 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()
Example #24
0
 def test_getPen(self):
     path = Path()
     pen = path.getPen()
     assert isinstance(pen, PathPen)
     assert id(pen) != id(path.getPen())
Example #25
0
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]
Example #26
0
    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
Example #27
0
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
Example #28
0
 def test_copy(self):
     path1 = Path()
     path2 = Path(path1)
     assert path1 == path2
Example #29
0
    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
Example #30
0
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)