From e6d85b99b1fb807b055e0977445e92704bea71a0 Mon Sep 17 00:00:00 2001 From: Rod Sheeter Date: Thu, 16 Jan 2020 23:03:18 -0800 Subject: [PATCH 01/57] not just bool --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4247acc..39270b5 100644 --- a/setup.py +++ b/setup.py @@ -402,7 +402,7 @@ def build_libraries(self, libraries): setup_params = dict( name="skia-pathops", use_scm_version={"write_to": version_file}, - description="Boolean operations on paths using the Skia library", + description="Python access to operations on paths using the Skia library", url="https://github.com/fonttools/skia-pathops", long_description=long_description, long_description_content_type="text/markdown", From 78dc70347459952d5fac218333950a09e547543f Mon Sep 17 00:00:00 2001 From: Rod Sheeter Date: Thu, 16 Jan 2020 23:03:50 -0800 Subject: [PATCH 02/57] kFill_InitStyle hates me --- src/python/pathops/_pathops.pxd | 2 ++ src/python/pathops/_pathops.pyx | 10 ++++++++++ src/python/pathops/_skia/core.pxd | 13 +++++++++++++ tests/pathops_test.py | 9 +++++++++ 4 files changed, 34 insertions(+) diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index b028ffc..d3ea2e0 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -122,6 +122,8 @@ cdef class Path: cpdef simplify(self, bint fix_winding=*, keep_starting_points=*) + cpdef stroke(self, width) + cdef list getVerbs(self) cdef list getPoints(self) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index e40fca7..997a4d9 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -3,6 +3,7 @@ from ._skia.core cimport ( SkPathFillType, SkPoint, SkScalar, + SkStrokeRec, SkRect, kMove_Verb, kLine_Verb, @@ -11,6 +12,7 @@ from ._skia.core cimport ( kCubic_Verb, kClose_Verb, kDone_Verb, + kFill_InitStyle, SK_ScalarNearlyZero, ) from ._skia.pathops cimport ( @@ -332,6 +334,14 @@ cdef class Path: if keep_starting_points: restore_starting_points(self, first_points) + cpdef stroke(self, width): + stroke_rec = new SkStrokeRec(kFill_InitStyle) + try: + stroke_rec.setStrokeStyle(width) + stroke_rec.applyToPath(&self.path, self.path) + finally: + del stroke_rec + cdef list getVerbs(self): cdef int i, count cdef uint8_t *verbs diff --git a/src/python/pathops/_skia/core.pxd b/src/python/pathops/_skia/core.pxd index 37c1602..a97eba7 100644 --- a/src/python/pathops/_skia/core.pxd +++ b/src/python/pathops/_skia/core.pxd @@ -149,3 +149,16 @@ cdef extern from "include/core/SkScalar.h": cdef enum: SK_ScalarNearlyZero + +cdef extern from "include/core/SkStrokeRec.h": + cdef cppclass SkStrokeRec: + SkStrokeRec(InitStyle style) + + void setStrokeStyle(SkScalar width, bint strokeAndFill = false) + + bint applyToPath(SkPath* dst, const SkPath& src) const; + +cdef extern from * namespace "SkStrokeRec": + enum InitStyle: + kHairline_InitStyle, + kFill_InitStyle diff --git a/tests/pathops_test.py b/tests/pathops_test.py index 99d1fb9..e84077c 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -689,3 +689,12 @@ def test_strip_collinear_moveTo(): expected.close() assert list(path) == list(expected) + +#TODO parameterize +def test_stroke_path(): + path = Path() + path.moveTo(5, 5) + path.lineTo(10, 5) + path.stroke(2) + + # TODO assert From b0e6dc08658798699b0018a471c86e47d7443501 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 17 Jan 2020 14:10:15 -0800 Subject: [PATCH 03/57] Add shell script to build libskia.so shared lib using gn/ninja --- build_skia.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100755 build_skia.sh diff --git a/build_skia.sh b/build_skia.sh new file mode 100755 index 0000000..9523a16 --- /dev/null +++ b/build_skia.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Run this script as `source ./build_skia.sh`. +# This way the LD_LIBRARY_PATH environment variable is imported in the current shell. +# NOTE: This was only tested on macOS. It requires python2 to be on the $PATH and +# it must be run *outside* of a python3 venv otherwise gn tool will complain... + +pushd src/cpp/skia + +python2 tools/git-sync-deps + +bin/gn gen out/Shared --args='is_official_build=true is_component_build=true is_debug=false skia_enable_pdf=false skia_enable_ccpr=false skia_enable_gpu=false skia_enable_discrete_gpu=false skia_enable_nvpr=false skia_enable_skottie=false skia_enable_skshaper=false skia_use_dng_sdk=false skia_use_expat=false skia_use_gl=false skia_use_harfbuzz=false skia_use_icu=false skia_use_libgifcodec=false skia_use_libjpeg_turbo=false skia_use_libwebp=false skia_use_piex=false skia_use_sfntly=false skia_use_xps=false skia_use_zlib=false skia_use_libpng=false' + +ninja -C out/Shared + +export LD_LIBRARY_PATH=$(pwd)/out/Shared + +popd From 177b8ec0f67cd429b4e621dfeb01745eaaf4debc Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 17 Jan 2020 14:11:18 -0800 Subject: [PATCH 04/57] cython doesn't seem to like cpp funcs default arguments --- src/python/pathops/_pathops.pyx | 2 +- src/python/pathops/_skia/core.pxd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 997a4d9..c333bf5 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -337,7 +337,7 @@ cdef class Path: cpdef stroke(self, width): stroke_rec = new SkStrokeRec(kFill_InitStyle) try: - stroke_rec.setStrokeStyle(width) + stroke_rec.setStrokeStyle(width, False) stroke_rec.applyToPath(&self.path, self.path) finally: del stroke_rec diff --git a/src/python/pathops/_skia/core.pxd b/src/python/pathops/_skia/core.pxd index a97eba7..a144023 100644 --- a/src/python/pathops/_skia/core.pxd +++ b/src/python/pathops/_skia/core.pxd @@ -154,7 +154,7 @@ cdef extern from "include/core/SkStrokeRec.h": cdef cppclass SkStrokeRec: SkStrokeRec(InitStyle style) - void setStrokeStyle(SkScalar width, bint strokeAndFill = false) + void setStrokeStyle(SkScalar width, bint strokeAndFill) bint applyToPath(SkPath* dst, const SkPath& src) const; From 9dde0bdde96a2b26794faa324ee46bc9a9169585 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 17 Jan 2020 14:12:47 -0800 Subject: [PATCH 05/57] setup.py: link extension to our src/cpp/skia/out/Shared/libskia.so Make sure you run 'source ./build_skia.sh' first. --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 39270b5..c001cab 100644 --- a/setup.py +++ b/setup.py @@ -390,6 +390,8 @@ def build_libraries(self, libraries): define_macros=define_macros, include_dirs=include_dirs, extra_compile_args=extra_compile_args, + libraries=["skia"], + library_dirs=["src/cpp/skia/out/Shared"], language="c++", ), ] @@ -411,7 +413,7 @@ def build_libraries(self, libraries): license="BSD-3-Clause", package_dir={"": pkg_dir}, packages=find_packages(pkg_dir), - libraries=libraries, + # libraries=libraries, ext_modules=extensions, cmdclass={ 'build_ext': custom_build_ext, From 4adb2645fa15bcce76fa69c4a0788371ed817db8 Mon Sep 17 00:00:00 2001 From: rsheeter Date: Fri, 17 Jan 2020 14:28:53 -0800 Subject: [PATCH 06/57] Update README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 9b3200d..fe726e7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,23 @@ Python bindings for the [Google Skia](https://skia.org) library's [Path Ops](https://skia.org/dev/present/pathops) module, performing boolean operations on paths (intersection, union, difference, xor). + +TEMPORARY: BUILD +================ +```shell + +Temporary: + +```shell +# setup and activate a py2 venv +pip install ninja +source ./build_skia.sh +deactivate +# activate py3 venv +python setup.py build_ext --inplace +pip install -e . +``` + Install ======= From ef1fd09af102f317c3c3a9cc69dbe86a60bedfba Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 17 Jan 2020 14:41:42 -0800 Subject: [PATCH 07/57] Update README.md Simplify build instructions, and move 'export LD_LIBRARY_PATH=...' outside of build_skia.sh --- README.md | 8 ++++---- build_skia.sh | 16 ++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fe726e7..c4d3bf0 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ TEMPORARY: BUILD Temporary: ```shell -# setup and activate a py2 venv -pip install ninja -source ./build_skia.sh -deactivate +# build Skia library +./build_skia.sh +# make sure the runtime linker can find it +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)/src/cpp/skia/out/Shared # activate py3 venv python setup.py build_ext --inplace pip install -e . diff --git a/build_skia.sh b/build_skia.sh index 9523a16..a06bf02 100755 --- a/build_skia.sh +++ b/build_skia.sh @@ -1,8 +1,12 @@ #!/bin/bash -# Run this script as `source ./build_skia.sh`. -# This way the LD_LIBRARY_PATH environment variable is imported in the current shell. -# NOTE: This was only tested on macOS. It requires python2 to be on the $PATH and -# it must be run *outside* of a python3 venv otherwise gn tool will complain... +# NOTE: This requires python2 and virtualenv + +if [ ! -d "venv2" ]; then + python2 -m virtualenv venv2 +fi +source venv2/bin/activate + +pip install ninja pushd src/cpp/skia @@ -12,6 +16,6 @@ bin/gn gen out/Shared --args='is_official_build=true is_component_build=true is_ ninja -C out/Shared -export LD_LIBRARY_PATH=$(pwd)/out/Shared - popd + +deactivate From 21d5448317e06f2a46c7aaf95d926c4cda9e116f Mon Sep 17 00:00:00 2001 From: rsheeter Date: Fri, 17 Jan 2020 20:30:20 -0800 Subject: [PATCH 08/57] confirm stroke works --- tests/pathops_test.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/tests/pathops_test.py b/tests/pathops_test.py index e84077c..8e8ba0a 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -690,11 +690,31 @@ def test_strip_collinear_moveTo(): assert list(path) == list(expected) -#TODO parameterize -def test_stroke_path(): + +@pytest.mark.parametrize( + "operations, expected", + [ + # stroke a line 2 units wide + ( + ( + ('moveTo', (5, 5)), + ('lineTo', (10, 5)), + ('stroke', (2,)), + ), + ( + ('moveTo', ((5., 4.),)), + ('lineTo', ((10., 4.),)), + ('lineTo', ((10., 6.),)), + ('lineTo', ((5., 6.),)), + ('lineTo', ((5., 4.),)), + ('closePath', ()), + ), + ) + ] +) +def test_stroke_path(operations, expected): path = Path() - path.moveTo(5, 5) - path.lineTo(10, 5) - path.stroke(2) + for op, args in operations: + getattr(path, op)(*args) - # TODO assert + assert tuple(path.segments) == expected From 014361569c3d3bd54e1eaa44be9c250052a4765c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 13:45:29 +0000 Subject: [PATCH 09/57] Add python2 script to build libskia.a --- build_skia.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++ build_skia.sh | 21 --------- 2 files changed, 120 insertions(+), 21 deletions(-) create mode 100755 build_skia.py delete mode 100755 build_skia.sh diff --git a/build_skia.py b/build_skia.py new file mode 100755 index 0000000..9691f83 --- /dev/null +++ b/build_skia.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python2 +import sys + +py_ver = sys.version_info[:2] +if py_ver > (2, 7): + sys.exit("python 2.7 is required; this is {}.{}".format(*py_ver)) + +import argparse +import os +import subprocess + + +# script to bootstrap virtualenv without requiring pip +GET_VIRTUALENV_URL = "https://asottile.github.io/get-virtualenv.py" + +ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) + +SKIA_SRC_DIR = os.path.join(ROOT_DIR, "src", "cpp", "skia") +SKIA_BUILD_ARGS = [ + "is_official_build=true", + "is_debug=false", + "skia_enable_pdf=false", + "skia_enable_ccpr=false", + "skia_enable_gpu=false", + "skia_enable_discrete_gpu=false", + "skia_enable_nvpr=false", + "skia_enable_skottie=false", + "skia_enable_skshaper=false", + "skia_enable_fontmgr_empty=true", + "skia_use_dng_sdk=false", + "skia_use_expat=false", + "skia_use_freetype=false", + "skia_use_fontconfig=false", + "skia_use_fonthost_mac=false", + "skia_use_gl=false", + "skia_use_harfbuzz=false", + "skia_use_icu=false", + "skia_use_libgifcodec=false", + "skia_use_libjpeg_turbo=false", + "skia_use_libpng=false", + "skia_use_libwebp=false", + "skia_use_piex=false", + "skia_use_sfntly=false", + "skia_use_xps=false", + "skia_use_zlib=false", +] + + +def make_virtualenv(venv_dir): + from contextlib import closing + import io + from urllib2 import urlopen + + exe = ".exe" if sys.platform == "win32" else "" + bin_dir = "Scripts" if sys.platform == "win32" else "bin" + venv_bin_dir = os.path.join(venv_dir, bin_dir) + python_exe = os.path.join(venv_bin_dir, "python" + exe) + + # bootstrap virtualenv if not already present + if not os.path.exists(python_exe): + tmp = io.BytesIO() + with closing(urlopen(GET_VIRTUALENV_URL)) as response: + tmp.write(response.read()) + + p = subprocess.Popen([sys.executable, "-", venv_dir], stdin=subprocess.PIPE) + p.communicate(tmp.getvalue()) + if p.returncode != 0: + sys.exit("failed to create virtualenv") + assert os.path.exists(python_exe) + + # pip install ninja + ninja_exe = os.path.join(venv_bin_dir, "ninja" + exe) + if not os.path.exists(ninja_exe): + subprocess.check_call( + [ + os.path.join(venv_bin_dir, "pip" + exe), + "install", + "--only-binary=ninja", + "ninja", + ] + ) + + # place virtualenv bin in front of $PATH, like 'source venv/bin/activate' + env = os.environ.copy() + env["PATH"] = os.pathsep.join([venv_bin_dir, env.get("PATH", "")]) + + return env + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "build_dir", + default=os.path.join("build", "skia"), + nargs="?", + help="directory where to build libskia (default: %(default)s)", + ) + args = parser.parse_args() + + build_dir = os.path.abspath(args.build_dir) + venv_dir = os.path.join(build_dir, "venv2") + + env = make_virtualenv(venv_dir) + + subprocess.check_call( + ["python", os.path.join("tools", "git-sync-deps")], env=env, cwd=SKIA_SRC_DIR + ) + + subprocess.check_call( + [ + os.path.join("bin", "gn"), + "gen", + build_dir, + "--args={}".format(" ".join(SKIA_BUILD_ARGS)), + ], + env=env, + cwd=SKIA_SRC_DIR, + ) + + subprocess.check_call(["ninja", "-C", build_dir], env=env) diff --git a/build_skia.sh b/build_skia.sh deleted file mode 100755 index a06bf02..0000000 --- a/build_skia.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# NOTE: This requires python2 and virtualenv - -if [ ! -d "venv2" ]; then - python2 -m virtualenv venv2 -fi -source venv2/bin/activate - -pip install ninja - -pushd src/cpp/skia - -python2 tools/git-sync-deps - -bin/gn gen out/Shared --args='is_official_build=true is_component_build=true is_debug=false skia_enable_pdf=false skia_enable_ccpr=false skia_enable_gpu=false skia_enable_discrete_gpu=false skia_enable_nvpr=false skia_enable_skottie=false skia_enable_skshaper=false skia_use_dng_sdk=false skia_use_expat=false skia_use_gl=false skia_use_harfbuzz=false skia_use_icu=false skia_use_libgifcodec=false skia_use_libjpeg_turbo=false skia_use_libwebp=false skia_use_piex=false skia_use_sfntly=false skia_use_xps=false skia_use_zlib=false skia_use_libpng=false' - -ninja -C out/Shared - -popd - -deactivate From ad3892abd8aa8eee3577a30f57a4bbc69eb48847 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 13:47:04 +0000 Subject: [PATCH 10/57] setup.py: link static skia from build/skia dir remove code to build skia with setuptools. One must run './build_skia.py' before setup.py --- setup.py | 93 +------------------------------------------------------- 1 file changed, 1 insertion(+), 92 deletions(-) diff --git a/setup.py b/setup.py index c001cab..e71934c 100644 --- a/setup.py +++ b/setup.py @@ -273,77 +273,6 @@ def build_libraries(self, libraries): cpp_dir = os.path.join("src", "cpp") skia_dir = os.path.join(cpp_dir, "skia") -skia_src = [ - os.path.join(skia_dir, "src", "core", "SkArenaAlloc.cpp"), - os.path.join(skia_dir, "src", "core", "SkBuffer.cpp"), - os.path.join(skia_dir, "src", "core", "SkCubicClipper.cpp"), - os.path.join(skia_dir, "src", "core", "SkData.cpp"), - os.path.join(skia_dir, "src", "core", "SkEdgeClipper.cpp"), - os.path.join(skia_dir, "src", "core", "SkGeometry.cpp"), - os.path.join(skia_dir, "src", "core", "SkLineClipper.cpp"), - os.path.join(skia_dir, "src", "core", "SkMath.cpp"), - os.path.join(skia_dir, "src", "core", "SkMatrix.cpp"), - os.path.join(skia_dir, "src", "core", "SkPath.cpp"), - os.path.join(skia_dir, "src", "core", "SkPathRef.cpp"), - os.path.join(skia_dir, "src", "core", "SkPoint.cpp"), - os.path.join(skia_dir, "src", "core", "SkRect.cpp"), - os.path.join(skia_dir, "src", "core", "SkRRect.cpp"), - os.path.join(skia_dir, "src", "core", "SkSemaphore.cpp"), - os.path.join(skia_dir, "src", "core", "SkString.cpp"), - os.path.join(skia_dir, "src", "core", "SkStringUtils.cpp"), - os.path.join(skia_dir, "src", "core", "SkUtils.cpp"), - os.path.join(skia_dir, "src", "core", "SkThreadID.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkAddIntersections.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkDConicLineIntersection.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkDCubicLineIntersection.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkDCubicToQuads.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkDLineIntersection.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkDQuadLineIntersection.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkIntersections.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkOpAngle.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkOpBuilder.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkOpCoincidence.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkOpContour.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkOpCubicHull.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkOpEdgeBuilder.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkOpSegment.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkOpSpan.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsAsWinding.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsCommon.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsConic.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsCubic.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsCurve.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsDebug.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsLine.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsOp.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsQuad.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsRect.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsSimplify.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsTightBounds.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsTSect.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsTypes.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathOpsWinding.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkPathWriter.cpp"), - os.path.join(skia_dir, "src", "pathops", "SkReduceOrder.cpp"), - os.path.join(skia_dir, "src", "utils", "SkUTF.cpp"), - os.path.join(skia_dir, "src", "ports", "SkDebug_stdio.cpp"), - os.path.join(skia_dir, "src", "ports", "SkMemory_malloc.cpp"), - os.path.join(skia_dir, "src", "ports", "SkOSFile_stdio.cpp"), - os.path.join(cpp_dir, "SkMallocThrow.cpp"), -] - -if os.name == "nt": - skia_src += [ - os.path.join(skia_dir, "src", "ports", "SkDebug_win.cpp"), - os.path.join(skia_dir, "src", "ports", "SkOSFile_win.cpp"), - ] -elif os.name == "posix": - skia_src += [ - os.path.join(skia_dir, "src", "ports", "SkOSFile_posix.cpp"), - ] -else: - raise RuntimeError("unsupported OS: %r" % os.name) - include_dirs = [os.path.join(skia_dir)] extra_compile_args = { @@ -360,24 +289,6 @@ def build_libraries(self, libraries): ], } -shared_macros = [ - ("SK_SUPPORT_GPU", "0"), -] -define_macros = { - "": shared_macros, -} - -libraries = [ - ( - 'skia', { - 'sources': skia_src, - 'include_dirs': include_dirs, - 'cflags': extra_compile_args, - 'macros': define_macros, - }, - ), -] - extensions = [ Extension( "pathops._pathops", @@ -387,11 +298,10 @@ def build_libraries(self, libraries): depends=[ os.path.join(skia_dir, 'include', 'pathops', 'SkPathOps.h'), ], - define_macros=define_macros, include_dirs=include_dirs, extra_compile_args=extra_compile_args, libraries=["skia"], - library_dirs=["src/cpp/skia/out/Shared"], + library_dirs=["build/skia"], language="c++", ), ] @@ -413,7 +323,6 @@ def build_libraries(self, libraries): license="BSD-3-Clause", package_dir={"": pkg_dir}, packages=find_packages(pkg_dir), - # libraries=libraries, ext_modules=extensions, cmdclass={ 'build_ext': custom_build_ext, From 4094156769a85084b64d0f016b006f1ed0321ac9 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 14:00:13 +0000 Subject: [PATCH 11/57] setup.py: run build_skia.py before building extension --- setup.py | 102 +++---------------------------------------------------- 1 file changed, 4 insertions(+), 98 deletions(-) diff --git a/setup.py b/setup.py index e71934c..6910494 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,11 @@ from __future__ import print_function from setuptools import setup, find_packages, Extension from setuptools.command.build_ext import build_ext -from distutils.command.build_clib import build_clib from distutils.errors import DistutilsSetupError from distutils import log -from distutils.dep_util import newer_group, newer_pairwise +from distutils.dep_util import newer_group import pkg_resources +import subprocess import sys import os import platform @@ -171,104 +171,11 @@ def build_extension(self, ext): target_lang=language) def run(self): - # Setuptools `develop` command (used by `pip install -e .`) only calls - # `build_ext`, unlike the `install` command which in turn calls `build` - # and all its related sub-commands. Linking the Cython extension module - # with the Skia static library fails because the `build_clib` command - # is not automatically called when doing an editable install. - # Here we make sure that `build_clib` command is always run before the - # the extension module is compiled, even when doing editable install. - # https://github.com/pypa/setuptools/issues/1040 - self.run_command("build_clib") + # make sure libskia.a static library is built before the extension + subprocess.run(["python2", "build_skia.py"]) build_ext.run(self) -class custom_build_clib(build_clib): - """ Custom build_clib command which allows to pass compiler-specific - 'macros' and 'cflags' when compiling C libraries. - - In the setup 'libraries' option, the 'macros' and 'cflags' can be - provided as dict with the compiler type as the key (e.g. "unix", - "mingw32", "msvc") and the value containing the list of macros/cflags. - A special empty string '' key may be used for default options that - apply to all the other compiler types except for those explicitly - listed. - """ - - def finalize_options(self): - build_clib.finalize_options(self) - if self.compiler is None: - # we use this variable with tox to build using GCC on Windows. - # https://bitbucket.org/hpk42/tox/issues/274/specify-compiler - self.compiler = os.environ.get("DISTUTILS_COMPILER", None) - - def build_libraries(self, libraries): - for (lib_name, build_info) in libraries: - sources = build_info.get('sources') - if sources is None or not isinstance(sources, (list, tuple)): - raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'sources' must be present and must be " - "a list of source filenames" % lib_name) - sources = list(sources) - - # detect target language - language = self.compiler.detect_language(sources) - - # do compiler specific customizations - compiler_type = self.compiler.compiler_type - - # strip compile flags that are not valid for C++ to avoid warnings - if compiler_type == "unix" and language == "c++": - if "-Wstrict-prototypes" in self.compiler.compiler_so: - self.compiler.compiler_so.remove("-Wstrict-prototypes") - - # get compiler-specific preprocessor definitions - macros = build_info.get("macros", []) - if isinstance(macros, dict): - if compiler_type in macros: - macros = macros[compiler_type] - else: - macros = macros.get("", []) - - include_dirs = build_info.get('include_dirs') - - # get compiler-specific compile flags - cflags = build_info.get("cflags", []) - if isinstance(cflags, dict): - if compiler_type in cflags: - cflags = cflags[compiler_type] - else: - cflags = cflags.get("", []) - - expected_objects = self.compiler.object_filenames( - sources, - output_dir=self.build_temp) - - # TODO: also support objects' dependencies - if (self.force or - newer_pairwise(sources, expected_objects) != ([], [])): - log.info("building '%s' library", lib_name) - # compile the source code to object files - objects = self.compiler.compile(sources, - output_dir=self.build_temp, - macros=macros, - include_dirs=include_dirs, - extra_postargs=cflags, - debug=self.debug) - else: - log.debug( - "skipping build '%s' objects (up-to-date)" % lib_name) - objects = expected_objects - - # Now "link" the object files together into a static library. - # (On Unix at least, this isn't really linking -- it just - # builds an archive. Whatever.) - self.compiler.create_static_lib(objects, lib_name, - output_dir=self.build_clib, - debug=self.debug) - - pkg_dir = os.path.join("src", "python") cpp_dir = os.path.join("src", "cpp") skia_dir = os.path.join(cpp_dir, "skia") @@ -326,7 +233,6 @@ def build_libraries(self, libraries): ext_modules=extensions, cmdclass={ 'build_ext': custom_build_ext, - 'build_clib': custom_build_clib, }, setup_requires=["setuptools_scm"] + wheel, install_requires=[ From 6d7a32f11a84b03cc0cb20e0d8971b70280bdcd6 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 15:15:01 +0000 Subject: [PATCH 12/57] README.md: remove temporary build instructions as longer needed --- README.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README.md b/README.md index c4d3bf0..9b3200d 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,6 @@ Python bindings for the [Google Skia](https://skia.org) library's [Path Ops](https://skia.org/dev/present/pathops) module, performing boolean operations on paths (intersection, union, difference, xor). - -TEMPORARY: BUILD -================ -```shell - -Temporary: - -```shell -# build Skia library -./build_skia.sh -# make sure the runtime linker can find it -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)/src/cpp/skia/out/Shared -# activate py3 venv -python setup.py build_ext --inplace -pip install -e . -``` - Install ======= From 256c420380af6e3b3f367ca2f7e757ebffdef976 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 15:17:59 +0000 Subject: [PATCH 13/57] Remove SkMallowThrow.cpp as no longer needed now that we build the complete libskia --- src/cpp/SkMallocThrow.cpp | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/cpp/SkMallocThrow.cpp diff --git a/src/cpp/SkMallocThrow.cpp b/src/cpp/SkMallocThrow.cpp deleted file mode 100644 index bd11e5c..0000000 --- a/src/cpp/SkMallocThrow.cpp +++ /dev/null @@ -1,10 +0,0 @@ -// This is to avoid importing src/core/SkMallocPixelRef.cpp (which implements -// among other things the `sk_malloc_throw` function below), as the latter -// has a long list of dependencies which we don't need for skia-pathops. -// The function is used by SkTArray.h, included by pathops/SkPathWriter.h. -#include "include/private/SkMalloc.h" -#include "src/core/SkSafeMath.h" - -void* sk_malloc_throw(size_t count, size_t elemSize) { - return sk_malloc_throw(SkSafeMath::Mul(count, elemSize)); -} From 2d1d1f5586b7d16da8c71d6843beb9d8a47a5279 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 15:27:28 +0000 Subject: [PATCH 14/57] travis: use manylinux2014 for c++14 support --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4a2c179..4d9f0cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,8 @@ env: - TEST_DEPENDS="tox" - PLAT=x86_64 - UNICODE_WIDTH=32 + # use 'manylinux2014' since Skia requires C++14 support + - MB_ML_VER=2014 - TWINE_USERNAME="anthrotype" - secure: O0cS/1sCRfjuDVdlMihyqyX6b7so3qZ41OhqDC5O5f4bvk87vno9GGk7U0ZfdoUqjnjXLd7QvEnax4vWJx/tvSEh/wJC07U2pcFDNehkYJIEZCf/MQWzESWd905fUSWP1/BbKgCWvfq7WZOH/3iKpDyQP5DKlrnoq3E2H2gYR3xKd7ASAZHtUYariE4bMEnjg4SDANfm7SHnlD5a/S4/IjgxU0DjCKKbkX7HbGUiCAjjr3j3z9amAhxCmoWyOKvNHjKegG2okEb08ERtcbyYWan0Eu5FqCDMkWwhQmACC1lXz0xHyHW4VZWDyQC1cDrSTirN9rNdamTnfqJPP1eURxGNmNqazrem77HAUKIuh5WjXLFZwKzp+KWMb5TTXYWIsh8gx/IAjGfPoi8nKOWd+bxWLeakDM4kka7pLJDsuRnWSWKzDaDDpMuFm76RzDJjTWCsva93l3EZ8/fkXQ3sGrVC7f8MAjaqBEs+vV6YZMv3WZuSfZkv2AVoPLKkPxWB3RDekKjAw/O8qovqDSGHVgV8XU+6AQVEWhEu8dEtEbXn0UPMQ/bSjQeugl5AkmmBC4iIisuP8rtPB+xdV/iQALEM/RdLJHzC76VQNuXMke69roK5+ZhA6TCCix7I3TIq+XtNQ78SnAUGuooND+WHxVVSQ5GK9DGiCMetRlOQkIA= From 125505dcde85641a9b614a543fd111717c781745 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 15:27:47 +0000 Subject: [PATCH 15/57] Update multibuild submodule --- multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multibuild b/multibuild index 88a0b6f..24d35a4 160000 --- a/multibuild +++ b/multibuild @@ -1 +1 @@ -Subproject commit 88a0b6f0eb770cf9f95792d66410e7696ce3d384 +Subproject commit 24d35a4af2583473a0ec224dbe1990dd1e0ace5d From 6a5fa685f5ade39c3bf1ffd15720fffa2bda19c7 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 15:31:56 +0000 Subject: [PATCH 16/57] travis: don't use custom docker image, use vanilla manylinux2014 image --- .travis.yml | 1 - config.sh | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4d9f0cb..b9594bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ env: global: - - DOCKER_IMAGE=anthrotype/manylinux:gcc9.2.0 # directory containing the project source - REPO_DIR=. # pip dependencies to _test_ project diff --git a/config.sh b/config.sh index 1f659cd..20a32c7 100644 --- a/config.sh +++ b/config.sh @@ -4,11 +4,7 @@ function pre_build { # Any stuff that you need to do before you start building the wheels # Runs in the root directory of this repository. - if [ -z "$IS_OSX" ]; then - export CFLAGS="-static-libstdc++" - export CC=/usr/local/gcc-9.2.0/bin/gcc-9.2.0 - export CXX=/usr/local/gcc-9.2.0/bin/g++-9.2.0 - fi + : } function run_tests { From bf4f58f41812259dbbeb98a25837e7f9d0bd897b Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 15:46:13 +0000 Subject: [PATCH 17/57] travis: try to downgrade pip as v20 seems to break multibuild https://github.com/matthew-brett/multibuild/issues/298 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b9594bf..7118191 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ env: # directory containing the project source - REPO_DIR=. # pip dependencies to _test_ project - - TEST_DEPENDS="tox" + - TEST_DEPENDS="tox pip==19.3.1" - PLAT=x86_64 - UNICODE_WIDTH=32 # use 'manylinux2014' since Skia requires C++14 support From 799009ddf55fd19a23b02da0432615090e4aa28f Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 16:12:32 +0000 Subject: [PATCH 18/57] setup.py: read PYTHON2_EXE from environment and export python2 path in appveyor.yml --- appveyor.yml | 1 + setup.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 8cfb706..08212d2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,7 @@ environment: TWINE_USERNAME: "anthrotype" TWINE_PASSWORD: secure: 9L/DdqoIILlN7qCh0lotvA== + PYTHON2_EXE: "C:\\Python27\\python.exe" matrix: - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6" diff --git a/setup.py b/setup.py index 6910494..9266a41 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,11 @@ import re +# Building libskia with its 'gn' build tool requires python2; if 'python2' +# executable is not in your $PATH, you can export PYTHON2_EXE=... before +# running setup.py script. +PYTHON2_EXE = os.environ.get("PYTHON2_EXE", "python2") + # check if minimum required Cython is available cython_version_re = re.compile('\s*"cython\s*>=\s*([0-9][0-9\w\.]*)\s*"') with open("pyproject.toml", "r", encoding="utf-8") as fp: @@ -172,7 +177,7 @@ def build_extension(self, ext): def run(self): # make sure libskia.a static library is built before the extension - subprocess.run(["python2", "build_skia.py"]) + subprocess.run([PYTHON2_EXE, "build_skia.py"]) build_ext.run(self) From 18fcb88b889b0c6dbbda05014a3151aeb1ddd86d Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 16:22:33 +0000 Subject: [PATCH 19/57] appveyor: build wheel in verbose mode --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 08212d2..6a01e71 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -59,7 +59,7 @@ install: - pip install --upgrade tox # build wheel - - pip wheel --no-deps --wheel-dir dist . + - pip wheel -v --no-deps --wheel-dir dist . # get the full path to the compiled wheel (ugly but works) - dir /s /b dist\skia_pathops*.whl > wheel.pth - set /p WHEEL_PATH= Date: Tue, 21 Jan 2020 16:22:52 +0000 Subject: [PATCH 20/57] setup.py: exit early if building libskia fails --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9266a41..dd18de6 100644 --- a/setup.py +++ b/setup.py @@ -177,7 +177,7 @@ def build_extension(self, ext): def run(self): # make sure libskia.a static library is built before the extension - subprocess.run([PYTHON2_EXE, "build_skia.py"]) + subprocess.run([PYTHON2_EXE, "build_skia.py"], check=True) build_ext.run(self) From 74e31d58ae137681eb75c8381c0d6b549ea5024d Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 16:32:08 +0000 Subject: [PATCH 21/57] build_skia.py: try appending .exe when calling gn on Windows --- build_skia.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/build_skia.py b/build_skia.py index 9691f83..6691d60 100755 --- a/build_skia.py +++ b/build_skia.py @@ -13,6 +13,8 @@ # script to bootstrap virtualenv without requiring pip GET_VIRTUALENV_URL = "https://asottile.github.io/get-virtualenv.py" +EXE_EXT = ".exe" if sys.platform == "win32" else "" + ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) SKIA_SRC_DIR = os.path.join(ROOT_DIR, "src", "cpp", "skia") @@ -51,10 +53,9 @@ def make_virtualenv(venv_dir): import io from urllib2 import urlopen - exe = ".exe" if sys.platform == "win32" else "" bin_dir = "Scripts" if sys.platform == "win32" else "bin" venv_bin_dir = os.path.join(venv_dir, bin_dir) - python_exe = os.path.join(venv_bin_dir, "python" + exe) + python_exe = os.path.join(venv_bin_dir, "python" + EXE_EXT) # bootstrap virtualenv if not already present if not os.path.exists(python_exe): @@ -69,11 +70,11 @@ def make_virtualenv(venv_dir): assert os.path.exists(python_exe) # pip install ninja - ninja_exe = os.path.join(venv_bin_dir, "ninja" + exe) + ninja_exe = os.path.join(venv_bin_dir, "ninja" + EXE_EXT) if not os.path.exists(ninja_exe): subprocess.check_call( [ - os.path.join(venv_bin_dir, "pip" + exe), + os.path.join(venv_bin_dir, "pip" + EXE_EXT), "install", "--only-binary=ninja", "ninja", @@ -108,7 +109,7 @@ def make_virtualenv(venv_dir): subprocess.check_call( [ - os.path.join("bin", "gn"), + os.path.join(SKIA_SRC_DIR, "bin", "gn" + EXE_EXT), "gen", build_dir, "--args={}".format(" ".join(SKIA_BUILD_ARGS)), From 7ed75af95a7952cd6e6bb8c4186543823d80afb2 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 17:15:38 +0000 Subject: [PATCH 22/57] build_skia.py: disable fontmgr_win --- build_skia.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build_skia.py b/build_skia.py index 6691d60..30b75e0 100755 --- a/build_skia.py +++ b/build_skia.py @@ -29,6 +29,8 @@ "skia_enable_skottie=false", "skia_enable_skshaper=false", "skia_enable_fontmgr_empty=true", + "skia_enable_fontmgr_win=false", + "skia_enable_fontmgr_win_gdi=false", "skia_use_dng_sdk=false", "skia_use_expat=false", "skia_use_freetype=false", From 67fb13c840294ac38bd83aed301797a59219a611 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 19:49:02 +0000 Subject: [PATCH 23/57] build_skia.py: add --shared-lib option --- build_skia.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/build_skia.py b/build_skia.py index 30b75e0..b0b44f4 100755 --- a/build_skia.py +++ b/build_skia.py @@ -98,6 +98,12 @@ def make_virtualenv(venv_dir): nargs="?", help="directory where to build libskia (default: %(default)s)", ) + parser.add_argument( + "-s", + "--shared-lib", + action="store_true", + help="build a shared library (default: static)" + ) args = parser.parse_args() build_dir = os.path.abspath(args.build_dir) @@ -109,12 +115,16 @@ def make_virtualenv(venv_dir): ["python", os.path.join("tools", "git-sync-deps")], env=env, cwd=SKIA_SRC_DIR ) + build_args = list(SKIA_BUILD_ARGS) + if args.shared_lib: + build_args.append("is_component_build=true") + subprocess.check_call( [ os.path.join(SKIA_SRC_DIR, "bin", "gn" + EXE_EXT), "gen", build_dir, - "--args={}".format(" ".join(SKIA_BUILD_ARGS)), + "--args={}".format(" ".join(build_args)), ], env=env, cwd=SKIA_SRC_DIR, From b439a54f881d231a41f11ea1a1b521ee630f6272 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Tue, 21 Jan 2020 20:01:10 +0000 Subject: [PATCH 24/57] setup.py: build DLL on windows and copy next to extension --- setup.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index dd18de6..68c4b2f 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,8 @@ from distutils.errors import DistutilsSetupError from distutils import log from distutils.dep_util import newer_group +from distutils.dir_util import mkpath +from distutils.file_util import copy_file import pkg_resources import subprocess import sys @@ -176,10 +178,50 @@ def build_extension(self, ext): target_lang=language) def run(self): - # make sure libskia.a static library is built before the extension - subprocess.run([PYTHON2_EXE, "build_skia.py"], check=True) + # make sure skia library is built before the extension module, inside the + # correct build_base dir (usually './build') + build_dir = os.path.join(self.get_finalized_command("build").build_base, "skia") + build_cmd = [PYTHON2_EXE, "build_skia.py", build_dir] + # for Windows, we want to build a shared skia.dll. If we build a static lib + # then gn/ninja pass the /MT flag (static runtime library) instead of /MD, + # and produce linker errors when building the python extension module + if sys.platform == "win32": + build_cmd.append("--shared-lib") + subprocess.run(build_cmd, check=True) + + # prepend the build dir to library_dirs so the linker can find our libskia + for ext in self.extensions: + ext.library_dirs.insert(0, build_dir) + build_ext.run(self) + # copy skia.dll next to the extension module + if sys.platform == "win32": + for ext in self.extensions: + for lib_name in ext.libraries: + for lib_dir in ext.library_dirs: + dll_filename = lib_name + ".dll" + dll_fullpath = os.path.join(lib_dir, dll_filename) + if os.path.exists(dll_fullpath): + break + else: + log.debug( + "cannot find '{}' in: {}".format( + dll_filename, ", ".join(ext.library_dirs) + ) + ) + continue + + ext_path = self.get_ext_fullpath(ext.name) + dest_dir = os.path.dirname(ext_path) + mkpath(dest_dir, verbose=self.verbose, dry_run=self.dry_run) + copy_file( + dll_fullpath, + os.path.join(dest_dir, dll_filename), + verbose=self.verbose, + dry_run=self.dry_run, + ) + pkg_dir = os.path.join("src", "python") cpp_dir = os.path.join("src", "cpp") @@ -213,7 +255,6 @@ def run(self): include_dirs=include_dirs, extra_compile_args=extra_compile_args, libraries=["skia"], - library_dirs=["build/skia"], language="c++", ), ] From 2b9b883f59a325747acbe3a475dc411d1a49ac55 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 22 Jan 2020 10:22:22 +0000 Subject: [PATCH 25/57] try to keep default values for win-specific options in hope that the linker errors will fix themselves... https://ci.appveyor.com/project/fonttools/skia-pathops/builds/30267082/job/x3qwi9lwlb8rrjtj --- build_skia.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/build_skia.py b/build_skia.py index b0b44f4..570cee4 100755 --- a/build_skia.py +++ b/build_skia.py @@ -29,8 +29,6 @@ "skia_enable_skottie=false", "skia_enable_skshaper=false", "skia_enable_fontmgr_empty=true", - "skia_enable_fontmgr_win=false", - "skia_enable_fontmgr_win_gdi=false", "skia_use_dng_sdk=false", "skia_use_expat=false", "skia_use_freetype=false", From bc0d7cf9f966706eb243f6cd8e29a039a1591d69 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 22 Jan 2020 11:01:22 +0000 Subject: [PATCH 26/57] setup.py: make custom build_ext more generic, less skia-specific --- setup.py | 92 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/setup.py b/setup.py index 68c4b2f..702d61f 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ from distutils.dep_util import newer_group from distutils.dir_util import mkpath from distutils.file_util import copy_file +from collections import namedtuple import pkg_resources import subprocess import sys @@ -177,50 +178,63 @@ def build_extension(self, ext): build_temp=self.build_temp, target_lang=language) - def run(self): - # make sure skia library is built before the extension module, inside the - # correct build_base dir (usually './build') - build_dir = os.path.join(self.get_finalized_command("build").build_base, "skia") - build_cmd = [PYTHON2_EXE, "build_skia.py", build_dir] - # for Windows, we want to build a shared skia.dll. If we build a static lib - # then gn/ninja pass the /MT flag (static runtime library) instead of /MD, - # and produce linker errors when building the python extension module - if sys.platform == "win32": - build_cmd.append("--shared-lib") - subprocess.run(build_cmd, check=True) + def get_libraries(self, ext): + build_base = self.get_finalized_command("build").build_base + for i, builder in enumerate(ext.libraries): + if callable(builder): + lib = builder(build_base) + ext.libraries[i] = lib.name + ext.library_dirs.append(lib.build_dir) - # prepend the build dir to library_dirs so the linker can find our libskia - for ext in self.extensions: - ext.library_dirs.insert(0, build_dir) + return build_ext.get_libraries(self, ext) + def run(self): build_ext.run(self) - - # copy skia.dll next to the extension module if sys.platform == "win32": - for ext in self.extensions: - for lib_name in ext.libraries: - for lib_dir in ext.library_dirs: - dll_filename = lib_name + ".dll" - dll_fullpath = os.path.join(lib_dir, dll_filename) - if os.path.exists(dll_fullpath): - break - else: - log.debug( - "cannot find '{}' in: {}".format( - dll_filename, ", ".join(ext.library_dirs) - ) + self._copy_windows_dlls() + + def _copy_windows_dlls(self): + # copy DLLs next to the extension module + for ext in self.extensions: + for lib_name in ext.libraries: + for lib_dir in ext.library_dirs: + dll_filename = lib_name + ".dll" + dll_fullpath = os.path.join(lib_dir, dll_filename) + if os.path.exists(dll_fullpath): + break + else: + log.debug( + "cannot find '{}' in: {}".format( + dll_filename, ", ".join(ext.library_dirs) ) - continue - - ext_path = self.get_ext_fullpath(ext.name) - dest_dir = os.path.dirname(ext_path) - mkpath(dest_dir, verbose=self.verbose, dry_run=self.dry_run) - copy_file( - dll_fullpath, - os.path.join(dest_dir, dll_filename), - verbose=self.verbose, - dry_run=self.dry_run, ) + continue + + ext_path = self.get_ext_fullpath(ext.name) + dest_dir = os.path.dirname(ext_path) + mkpath(dest_dir, verbose=self.verbose, dry_run=self.dry_run) + copy_file( + dll_fullpath, + os.path.join(dest_dir, dll_filename), + verbose=self.verbose, + dry_run=self.dry_run, + ) + + +_LibraryOutput = namedtuple("_LibraryOutput", "name build_dir") + + +def build_skia(build_base): + log.info("building 'skia' library") + build_dir = os.path.join(build_base, "skia") + build_cmd = [PYTHON2_EXE, "build_skia.py", build_dir] + # for Windows, we want to build a shared skia.dll. If we build a static lib + # then gn/ninja pass the /MT flag (static runtime library) instead of /MD, + # and produce linker errors when building the python extension module + if sys.platform == "win32": + build_cmd.append("--shared-lib") + subprocess.run(build_cmd, check=True) + return _LibraryOutput("skia", build_dir) pkg_dir = os.path.join("src", "python") @@ -254,7 +268,7 @@ def run(self): ], include_dirs=include_dirs, extra_compile_args=extra_compile_args, - libraries=["skia"], + libraries=[build_skia], language="c++", ), ] From 47c3b511043bfcbc90c6d37156d89d0ff4584652 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 22 Jan 2020 11:06:38 +0000 Subject: [PATCH 27/57] build_skia: pass --no-download to virtualenv --- build_skia.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) mode change 100755 => 100644 build_skia.py diff --git a/build_skia.py b/build_skia.py old mode 100755 new mode 100644 index 570cee4..c8503b9 --- a/build_skia.py +++ b/build_skia.py @@ -63,7 +63,9 @@ def make_virtualenv(venv_dir): with closing(urlopen(GET_VIRTUALENV_URL)) as response: tmp.write(response.read()) - p = subprocess.Popen([sys.executable, "-", venv_dir], stdin=subprocess.PIPE) + p = subprocess.Popen( + [sys.executable, "-", "--no-download", venv_dir], stdin=subprocess.PIPE + ) p.communicate(tmp.getvalue()) if p.returncode != 0: sys.exit("failed to create virtualenv") From edad6228ad111ce2c58087902cf6189a1402a3ad Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 22 Jan 2020 11:28:57 +0000 Subject: [PATCH 28/57] try to fix skia.dll linker issues... --- build_skia.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/build_skia.py b/build_skia.py index c8503b9..ceb728b 100644 --- a/build_skia.py +++ b/build_skia.py @@ -23,12 +23,10 @@ "is_debug=false", "skia_enable_pdf=false", "skia_enable_ccpr=false", - "skia_enable_gpu=false", "skia_enable_discrete_gpu=false", "skia_enable_nvpr=false", "skia_enable_skottie=false", "skia_enable_skshaper=false", - "skia_enable_fontmgr_empty=true", "skia_use_dng_sdk=false", "skia_use_expat=false", "skia_use_freetype=false", @@ -46,6 +44,16 @@ "skia_use_xps=false", "skia_use_zlib=false", ] +if sys.platform != "win32": + # On Linux, I need this flag otherwise I get undefined symbol upon importing; + # on Windows, defining this flag creates other linker issues (SkFontMgr being + # redefined by SkFontMgr_win_dw_factory.obj)... + SKIA_BUILD_ARGS.append("skia_enable_fontmgr_empty=true") + # We don't need GPU, but disabling this on Windows creates lots of undefined + # symbols upon linking the skia.dll, coming from SkWGL_win.cpp. The latter + # seems to require OpenGL32.lib, but this is only linked in when + # skia_enable_gpu=true (default), so I keep it on for Windows... + SKIA_BUILD_ARGS.append("skia_enable_gpu=false") def make_virtualenv(venv_dir): From 234c9f55bf11f86f58b04cb0777b1685470df9c7 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 22 Jan 2020 11:37:44 +0000 Subject: [PATCH 29/57] setup.py: passing a function to Extension.libraries causes a TypeError in Cython https://travis-ci.org/fonttools/skia-pathops/jobs/640403201 --- setup.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 702d61f..2cb5123 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ from distutils.dep_util import newer_group from distutils.dir_util import mkpath from distutils.file_util import copy_file -from collections import namedtuple import pkg_resources import subprocess import sys @@ -180,11 +179,10 @@ def build_extension(self, ext): def get_libraries(self, ext): build_base = self.get_finalized_command("build").build_base - for i, builder in enumerate(ext.libraries): - if callable(builder): - lib = builder(build_base) - ext.libraries[i] = lib.name - ext.library_dirs.append(lib.build_dir) + for library in ext.libraries: + if library in LIBRARY_BUILDERS: + library_dir = LIBRARY_BUILDERS[library](build_base) + ext.library_dirs.append(library_dir) return build_ext.get_libraries(self, ext) @@ -221,9 +219,6 @@ def _copy_windows_dlls(self): ) -_LibraryOutput = namedtuple("_LibraryOutput", "name build_dir") - - def build_skia(build_base): log.info("building 'skia' library") build_dir = os.path.join(build_base, "skia") @@ -234,7 +229,12 @@ def build_skia(build_base): if sys.platform == "win32": build_cmd.append("--shared-lib") subprocess.run(build_cmd, check=True) - return _LibraryOutput("skia", build_dir) + return build_dir + + +LIBRARY_BUILDERS = { + "skia": build_skia, +} pkg_dir = os.path.join("src", "python") @@ -268,7 +268,7 @@ def build_skia(build_base): ], include_dirs=include_dirs, extra_compile_args=extra_compile_args, - libraries=[build_skia], + libraries=["skia"], language="c++", ), ] From 6b7b11c0a94a99c1e50ba08a573ecf8280dfcd5e Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 22 Jan 2020 12:26:56 +0000 Subject: [PATCH 30/57] don't disable skia_use_gl on windows, in attempt to fix linker errors https://ci.appveyor.com/project/fonttools/skia-pathops/builds/30282031/job/w90uijye9597mj47#L3543 --- build_skia.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/build_skia.py b/build_skia.py index ceb728b..2ff7fff 100644 --- a/build_skia.py +++ b/build_skia.py @@ -32,7 +32,6 @@ "skia_use_freetype=false", "skia_use_fontconfig=false", "skia_use_fonthost_mac=false", - "skia_use_gl=false", "skia_use_harfbuzz=false", "skia_use_icu=false", "skia_use_libgifcodec=false", @@ -49,11 +48,10 @@ # on Windows, defining this flag creates other linker issues (SkFontMgr being # redefined by SkFontMgr_win_dw_factory.obj)... SKIA_BUILD_ARGS.append("skia_enable_fontmgr_empty=true") - # We don't need GPU, but disabling this on Windows creates lots of undefined - # symbols upon linking the skia.dll, coming from SkWGL_win.cpp. The latter - # seems to require OpenGL32.lib, but this is only linked in when - # skia_enable_gpu=true (default), so I keep it on for Windows... + # We don't need GPU or GL support, but disabling this on Windows creates lots + # of undefined symbols upon linking the skia.dll, so I keep them for Windows... SKIA_BUILD_ARGS.append("skia_enable_gpu=false") + SKIA_BUILD_ARGS.append("skia_use_gl=false") def make_virtualenv(venv_dir): From 439d1d5a31b85e2285f5f8e92cad782d4797c815 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 09:38:35 +0000 Subject: [PATCH 31/57] enabling gpu on windows support seems to require ccpr https://ci.appveyor.com/project/fonttools/skia-pathops/builds/30282984/job/2m2pqlvbvm3sj9sb#L3571 --- build_skia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_skia.py b/build_skia.py index 2ff7fff..d22be3b 100644 --- a/build_skia.py +++ b/build_skia.py @@ -22,7 +22,6 @@ "is_official_build=true", "is_debug=false", "skia_enable_pdf=false", - "skia_enable_ccpr=false", "skia_enable_discrete_gpu=false", "skia_enable_nvpr=false", "skia_enable_skottie=false", @@ -52,6 +51,7 @@ # of undefined symbols upon linking the skia.dll, so I keep them for Windows... SKIA_BUILD_ARGS.append("skia_enable_gpu=false") SKIA_BUILD_ARGS.append("skia_use_gl=false") + SKIA_BUILD_ARGS.append("skia_enable_ccpr=false") def make_virtualenv(venv_dir): From 07f73779a6ac00f3de03680b9b4fccb0c25a2e49 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 10:29:31 +0000 Subject: [PATCH 32/57] rename skia.dll.lib to skia.lib so we can link with extension module https://ci.appveyor.com/project/fonttools/skia-pathops/builds/30304441/job/ak0sq96eioi8h6nj#L3601 --- build_skia.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build_skia.py b/build_skia.py index d22be3b..de3c981 100644 --- a/build_skia.py +++ b/build_skia.py @@ -6,6 +6,7 @@ sys.exit("python 2.7 is required; this is {}.{}".format(*py_ver)) import argparse +import glob import os import subprocess @@ -137,3 +138,10 @@ def make_virtualenv(venv_dir): ) subprocess.check_call(["ninja", "-C", build_dir], env=env) + + # when building skia.dll on windows with gn and ninja, the DLL import file + # is written as 'skia.dll.lib'; however, when linking it with the extension + # module, setuptools expects it to be named 'skia.lib'. + if sys.platform == "win32" and args.shared_lib: + for f in glob.glob(os.path.join(build_dir, "skia.dll.*")): + os.rename(f, f.rsplit(".")[0]) From 707ede14d346f79bf03a5796c9c5edb7e75bed6c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 10:38:20 +0000 Subject: [PATCH 33/57] fix renaming skia.dll.lib to skia.lib --- build_skia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_skia.py b/build_skia.py index de3c981..a65bce4 100644 --- a/build_skia.py +++ b/build_skia.py @@ -144,4 +144,4 @@ def make_virtualenv(venv_dir): # module, setuptools expects it to be named 'skia.lib'. if sys.platform == "win32" and args.shared_lib: for f in glob.glob(os.path.join(build_dir, "skia.dll.*")): - os.rename(f, f.rsplit(".")[0]) + os.rename(f, f.replace(".dll", "")) From c43b807c84be4ad9f552f1e028c99130c0f274bd Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 12:19:02 +0000 Subject: [PATCH 34/57] setup.py: call the right vcvarsall.bat to we can build 32-bit --- setup.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 2cb5123..a5ff0ac 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ from distutils.dir_util import mkpath from distutils.file_util import copy_file import pkg_resources +import struct import subprocess import sys import os @@ -223,12 +224,21 @@ def build_skia(build_base): log.info("building 'skia' library") build_dir = os.path.join(build_base, "skia") build_cmd = [PYTHON2_EXE, "build_skia.py", build_dir] - # for Windows, we want to build a shared skia.dll. If we build a static lib - # then gn/ninja pass the /MT flag (static runtime library) instead of /MD, - # and produce linker errors when building the python extension module + + env = os.environ.copy() if sys.platform == "win32": + from distutils._msvccompiler import _get_vc_env + + # for Windows, we want to build a shared skia.dll. If we build a static lib + # then gn/ninja pass the /MT flag (static runtime library) instead of /MD, + # and produce linker errors when building the python extension module build_cmd.append("--shared-lib") - subprocess.run(build_cmd, check=True) + + # update Visual C++ toolchain environment depending on python architecture + arch = "x64" if struct.calcsize("P") * 8 == 64 else "x86" + env.update(_get_vc_env(arch)) + + subprocess.run(build_cmd, check=True, env=env) return build_dir From 989b7de30e12e53e49bf596ddc60813fdcf8762c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 12:22:06 +0000 Subject: [PATCH 35/57] empty commit to trigger appveyor From b5cccb552efee3577a1ab1056e89553fd2510ac0 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 16:09:00 +0000 Subject: [PATCH 36/57] point skia submodule to our fork patched for windows x86 --- src/cpp/skia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cpp/skia b/src/cpp/skia index 3517aa7..d6b2ec9 160000 --- a/src/cpp/skia +++ b/src/cpp/skia @@ -1 +1 @@ -Subproject commit 3517aa7b14ad52aa662bf38932f2ba358a9f8318 +Subproject commit d6b2ec9407bb8fa798d97524a84b4d0e5f93146e From 16ab9cd6c5a3e273ddba96fcd4ab0bbe985e834c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 16:21:22 +0000 Subject: [PATCH 37/57] build_skia: add --target-cpu option --- build_skia.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build_skia.py b/build_skia.py index a65bce4..15e543e 100644 --- a/build_skia.py +++ b/build_skia.py @@ -111,6 +111,12 @@ def make_virtualenv(venv_dir): action="store_true", help="build a shared library (default: static)" ) + parser.add_argument( + "--target-cpu", + default=None, + help="The desired CPU architecture for the build (default: host)", + choices=["x86", "x64", "arm", "arm64", "mipsel"] + ) args = parser.parse_args() build_dir = os.path.abspath(args.build_dir) @@ -125,6 +131,8 @@ def make_virtualenv(venv_dir): build_args = list(SKIA_BUILD_ARGS) if args.shared_lib: build_args.append("is_component_build=true") + if args.target_cpu: + build_args.append('target_cpu="{}"'.format(args.target_cpu)) subprocess.check_call( [ From 1137322354063dd0e513fb795b2c4d83b9a45335 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 16:22:13 +0000 Subject: [PATCH 38/57] setup.py: pass x64 or x86 as target_cpu depending on python arch --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index a5ff0ac..2863ea9 100644 --- a/setup.py +++ b/setup.py @@ -238,6 +238,8 @@ def build_skia(build_base): arch = "x64" if struct.calcsize("P") * 8 == 64 else "x86" env.update(_get_vc_env(arch)) + build_cmd.extend(["--target-cpu", arch]) + subprocess.run(build_cmd, check=True, env=env) return build_dir From efaadbe428fb15eaf17081fed936708336a16237 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 16:28:49 +0000 Subject: [PATCH 39/57] appveyor: move 32-bit build higher up so we can get errors earlier instead of waiting half an hour.... :( --- appveyor.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 6a01e71..4672a98 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,30 +9,29 @@ environment: secure: 9L/DdqoIILlN7qCh0lotvA== PYTHON2_EXE: "C:\\Python27\\python.exe" matrix: + - PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6" + PYTHON_ARCH: "32" + - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python37" PYTHON_VERSION: "3.7" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python38-x64" - PYTHON_VERSION: "3.8" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python37" + - PYTHON: "C:\\Python37-x64" PYTHON_VERSION: "3.7" - PYTHON_ARCH: "32" + PYTHON_ARCH: "64" - PYTHON: "C:\\Python38" PYTHON_VERSION: "3.8" PYTHON_ARCH: "32" + - PYTHON: "C:\\Python38-x64" + PYTHON_VERSION: "3.8" + PYTHON_ARCH: "64" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" From ded85f4b5319736c56892dafc225a147e0fc306f Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 16:38:03 +0000 Subject: [PATCH 40/57] appveyor: try to compile with Visual Studio 2019 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 4672a98..18c67b8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -image: Visual Studio 2017 +image: Visual Studio 2019 platform: x64 configuration: Release From 67299d7f40f444bd56e32c45d975e3dcd0dd6773 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 17:56:10 +0000 Subject: [PATCH 41/57] setup.py: allow to skip building skia and set custom library_dirs via env vars --- setup.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 2863ea9..8dd253a 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,12 @@ import re +# export BUILD_SKIA_FROM_SOURCE=0 to not build libskia when building extension +BUILD_SKIA_FROM_SOURCE = bool(int(os.environ.get("BUILD_SKIA_FROM_SOURCE", "1"))) +# Use this to specify the directory where your pre-built skia is located +SKIA_LIBRARY_DIR = os.environ.get("SKIA_LIBRARY_DIR") + + # Building libskia with its 'gn' build tool requires python2; if 'python2' # executable is not in your $PATH, you can export PYTHON2_EXE=... before # running setup.py script. @@ -69,6 +75,20 @@ class custom_build_ext(build_ext): listed. """ + _library_builders = {} + + @classmethod + def register_library_builder(cls, library_name, builder): + """Associates a builder function with signature `func(str) -> str` to + the given library_name. The builder is a callable that takes one + parameter, a build directory (e.g. './build'), and returns the full + directory path where the newly built library is located (e.g. a sub- + directory of the base build dir). + Builder functions will be called in `get_libraries` method. + E.g. see `build_skia` function defined below. + """ + cls._library_builders[library_name] = builder + def finalize_options(self): if with_cython: # compile *.pyx source files to *.cpp using cythonize @@ -179,10 +199,13 @@ def build_extension(self, ext): target_lang=language) def get_libraries(self, ext): - build_base = self.get_finalized_command("build").build_base + """Build all libraries for which a builder function is registered, + and append the resulting directory path to the extension module's + 'library_dirs' list so that the linker can find. + """ for library in ext.libraries: - if library in LIBRARY_BUILDERS: - library_dir = LIBRARY_BUILDERS[library](build_base) + if library in self._library_builders: + library_dir = self._library_builders[library](self.build_temp) ext.library_dirs.append(library_dir) return build_ext.get_libraries(self, ext) @@ -222,7 +245,7 @@ def _copy_windows_dlls(self): def build_skia(build_base): log.info("building 'skia' library") - build_dir = os.path.join(build_base, "skia") + build_dir = os.path.join(build_base, "src", "cpp", "skia") build_cmd = [PYTHON2_EXE, "build_skia.py", build_dir] env = os.environ.copy() @@ -244,9 +267,8 @@ def build_skia(build_base): return build_dir -LIBRARY_BUILDERS = { - "skia": build_skia, -} +if BUILD_SKIA_FROM_SOURCE: + custom_build_ext.register_library_builder("skia", build_skia) pkg_dir = os.path.join("src", "python") @@ -269,6 +291,8 @@ def build_skia(build_base): ], } +library_dirs = [SKIA_LIBRARY_DIR] if SKIA_LIBRARY_DIR is not None else [] + extensions = [ Extension( "pathops._pathops", @@ -281,6 +305,7 @@ def build_skia(build_base): include_dirs=include_dirs, extra_compile_args=extra_compile_args, libraries=["skia"], + library_dirs=library_dirs, language="c++", ), ] From 17d18bee738abc9a88a7f3711ec606d124554196 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 23 Jan 2020 19:42:48 +0000 Subject: [PATCH 42/57] setup.py: add setuptools_git_ls_files to setup_requires https://github.com/anthrotype/setuptools_git_ls_files uses git ls-files --recurse-submodule to include all the submodules in the sdist archive. setuptools_scm has a similar feature, but it currently doesn't support submodules. We need to include skia submodule in the source distribution to be able to build from it. --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8dd253a..7fa8527 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,8 @@ needs_wheel = {'bdist_wheel'}.intersection(argv) wheel = ['wheel'] if needs_wheel else [] +setuptools_git_ls_files = ["setuptools_git_ls_files"] if os.path.isdir(".git") else [] + class custom_build_ext(build_ext): """ Custom 'build_ext' command which allows to pass compiler-specific @@ -331,7 +333,7 @@ def build_skia(build_base): cmdclass={ 'build_ext': custom_build_ext, }, - setup_requires=["setuptools_scm"] + wheel, + setup_requires=["setuptools_scm"] + setuptools_git_ls_files + wheel, install_requires=[ ], extras_require={ From 4a034d19f72339fd2266930cbc804c849d3af8e5 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 24 Jan 2020 12:16:37 +0000 Subject: [PATCH 43/57] update multibuild submodule; fixed issue with pip >= 20.0 --- .travis.yml | 2 +- multibuild | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7118191..b9594bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ env: # directory containing the project source - REPO_DIR=. # pip dependencies to _test_ project - - TEST_DEPENDS="tox pip==19.3.1" + - TEST_DEPENDS="tox" - PLAT=x86_64 - UNICODE_WIDTH=32 # use 'manylinux2014' since Skia requires C++14 support diff --git a/multibuild b/multibuild index 24d35a4..6b0ddb5 160000 --- a/multibuild +++ b/multibuild @@ -1 +1 @@ -Subproject commit 24d35a4af2583473a0ec224dbe1990dd1e0ace5d +Subproject commit 6b0ddb5281f59d976c8026c082c9d73faf274790 From ccddd4816a5ed44083961ca871e386ec9cab9946 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 24 Jan 2020 12:45:09 +0000 Subject: [PATCH 44/57] Update skia submodule to chrome/m80 branch https://github.com/fonttools/skia/pull/new/chrome/m80-fix-win-x86 This branch includes the patch for gn/toolchain/BUILD.gn to allow building on Windows 32-bit with local Visual Studio toolchain. --- src/cpp/skia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cpp/skia b/src/cpp/skia index d6b2ec9..1c9ebb5 160000 --- a/src/cpp/skia +++ b/src/cpp/skia @@ -1 +1 @@ -Subproject commit d6b2ec9407bb8fa798d97524a84b4d0e5f93146e +Subproject commit 1c9ebb50024f80f3bf289838298e15185d8f6966 From 47684c6d542c65cdac62f5c4614aeedf7916f364 Mon Sep 17 00:00:00 2001 From: Rod S Date: Mon, 27 Jan 2020 11:22:09 -0800 Subject: [PATCH 45/57] declare setStrokeParams --- src/python/pathops/_skia/core.pxd | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/python/pathops/_skia/core.pxd b/src/python/pathops/_skia/core.pxd index a144023..c3b669c 100644 --- a/src/python/pathops/_skia/core.pxd +++ b/src/python/pathops/_skia/core.pxd @@ -150,11 +150,23 @@ cdef extern from "include/core/SkScalar.h": cdef enum: SK_ScalarNearlyZero +cdef extern from "include/core/SkPaint.h": + enum SkLineCap: + kButt_Cap "SkPaint::Cap::kButt_Cap", + kRound_Cap "SkPaint::Cap::kRound_Cap", + kSquare_Cap "SkPaint::Cap::kSquare_Cap" + + enum SkLineJoin: + kMiter_Join "SkPaint::Join::kMiter_Join", + kRound_Join "SkPaint::Join::kRound_Join", + kBevel_Join "SkPaint::Join::kBevel_Join" + cdef extern from "include/core/SkStrokeRec.h": cdef cppclass SkStrokeRec: SkStrokeRec(InitStyle style) void setStrokeStyle(SkScalar width, bint strokeAndFill) + void setStrokeParams(SkLineCap cap, SkLineJoin join, SkScalar miterLimit) bint applyToPath(SkPath* dst, const SkPath& src) const; From b1fad0289176c4892bfb7b76f37a11c50bad6fee Mon Sep 17 00:00:00 2001 From: Rod S Date: Mon, 27 Jan 2020 11:26:30 -0800 Subject: [PATCH 46/57] expose decompose quad --- src/python/pathops/__init__.py | 1 + src/python/pathops/_pathops.pxd | 2 +- src/python/pathops/_pathops.pyx | 8 ++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/python/pathops/__init__.py b/src/python/pathops/__init__.py index 5646f20..40fd0e0 100644 --- a/src/python/pathops/__init__.py +++ b/src/python/pathops/__init__.py @@ -12,6 +12,7 @@ OpenPathError, bits2float, float2bits, + decompose_quadratic_segment, ) from .operations import ( diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index d3ea2e0..c42f994 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -229,7 +229,7 @@ cpdef int restore_starting_points(Path path, list points) except -1 cpdef bint winding_from_even_odd(Path path, bint truetype=*) except False -cdef list decompose_quadratic_segment(tuple points) +cdef list _decompose_quadratic_segment(tuple points) cdef int find_oncurve_point( diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index c333bf5..cda11e6 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -706,7 +706,7 @@ cdef class PathPen: pt3[0], pt3[1]) def qCurveTo(self, *points): - for pt1, pt2 in decompose_quadratic_segment(points): + for pt1, pt2 in _decompose_quadratic_segment(points): self._qCurveToOne(pt1, pt2) cdef _qCurveToOne(self, pt1, pt2): @@ -1031,7 +1031,11 @@ cpdef bint winding_from_even_odd(Path path, bint truetype=False) except False: return True -cdef list decompose_quadratic_segment(tuple points): +def decompose_quadratic_segment(points): + return _decompose_quadratic_segment(points) + + +cdef list _decompose_quadratic_segment(tuple points): cdef: int i, n = len(points) - 1 list quad_segments = [] From 99ffb83db35f29a22edd323474e4a6ba8ab53dbe Mon Sep 17 00:00:00 2001 From: Rod S Date: Mon, 27 Jan 2020 19:20:29 -0800 Subject: [PATCH 47/57] pencil in setStrokeParams --- src/python/pathops/_pathops.pxd | 15 ++++++++++++++- src/python/pathops/_pathops.pyx | 9 ++++++--- src/python/pathops/_skia/core.pxd | 9 +++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index c42f994..cb87993 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -1,4 +1,6 @@ from ._skia.core cimport ( + SkLineCap, + SkLineJoin, SkPath, SkPathFillType, SkPoint, @@ -38,6 +40,17 @@ cpdef enum FillType: INVERSE_EVEN_ODD = SkPathFillType.kInverseEvenOdd +cpdef enum LineCap: + BUTT_CAP = SkLineCap.kButt_Cap, + ROUND_CAP = SkLineCap.kRound_Cap, + SQUARE_CAP = SkLineCap.kSquare_Cap + +cpdef enum LineJoin: + MITER_JOIN = SkLineJoin.kMiter_Join, + ROUND_JOIN = SkLineJoin.kRound_Join, + BEVEL_JOIN = SkLineJoin.kBevel_Join + + cdef union FloatIntUnion: float Float int32_t SignBitInt @@ -122,7 +135,7 @@ cdef class Path: cpdef simplify(self, bint fix_winding=*, keep_starting_points=*) - cpdef stroke(self, width) + cpdef stroke(self, SkScalar width, LineCap cap, LineJoin join, SkScalar miter_limit) cdef list getVerbs(self) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index cda11e6..9f02cbf 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -5,6 +5,8 @@ from ._skia.core cimport ( SkScalar, SkStrokeRec, SkRect, + SkLineCap, + SkLineJoin, kMove_Verb, kLine_Verb, kQuad_Verb, @@ -334,11 +336,12 @@ cdef class Path: if keep_starting_points: restore_starting_points(self, first_points) - cpdef stroke(self, width): + cpdef stroke(self, SkScalar width, LineCap cap, LineJoin join, SkScalar miter_limit): stroke_rec = new SkStrokeRec(kFill_InitStyle) try: - stroke_rec.setStrokeStyle(width, False) - stroke_rec.applyToPath(&self.path, self.path) + stroke_rec.setStrokeStyle(width, False) + stroke_rec.setStrokeParams(cap, join, miter_limit) + stroke_rec.applyToPath(&self.path, self.path) finally: del stroke_rec diff --git a/src/python/pathops/_skia/core.pxd b/src/python/pathops/_skia/core.pxd index c3b669c..481b04c 100644 --- a/src/python/pathops/_skia/core.pxd +++ b/src/python/pathops/_skia/core.pxd @@ -150,26 +150,27 @@ cdef extern from "include/core/SkScalar.h": cdef enum: SK_ScalarNearlyZero + cdef extern from "include/core/SkPaint.h": - enum SkLineCap: + enum SkLineCap "SkPaint::Cap": kButt_Cap "SkPaint::Cap::kButt_Cap", kRound_Cap "SkPaint::Cap::kRound_Cap", kSquare_Cap "SkPaint::Cap::kSquare_Cap" - enum SkLineJoin: + enum SkLineJoin "SkPaint::Join": kMiter_Join "SkPaint::Join::kMiter_Join", kRound_Join "SkPaint::Join::kRound_Join", kBevel_Join "SkPaint::Join::kBevel_Join" + cdef extern from "include/core/SkStrokeRec.h": cdef cppclass SkStrokeRec: SkStrokeRec(InitStyle style) - void setStrokeStyle(SkScalar width, bint strokeAndFill) void setStrokeParams(SkLineCap cap, SkLineJoin join, SkScalar miterLimit) - bint applyToPath(SkPath* dst, const SkPath& src) const; + cdef extern from * namespace "SkStrokeRec": enum InitStyle: kHairline_InitStyle, From 5c4e12fdf26c4b79309c5fbef78d323875e73d8e Mon Sep 17 00:00:00 2001 From: Rod S Date: Mon, 27 Jan 2020 19:34:32 -0800 Subject: [PATCH 48/57] expose cap/join enums --- src/python/pathops/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/python/pathops/__init__.py b/src/python/pathops/__init__.py index 40fd0e0..9e39e59 100644 --- a/src/python/pathops/__init__.py +++ b/src/python/pathops/__init__.py @@ -4,6 +4,8 @@ PathVerb, PathOp, FillType, + LineCap, + LineJoin, op, simplify, OpBuilder, From 693cd1e17766c7c0f3ff6df2d23dc1badb44bdb3 Mon Sep 17 00:00:00 2001 From: Rod S Date: Mon, 27 Jan 2020 20:41:08 -0800 Subject: [PATCH 49/57] Rough out a test for conic conversion. Fix path tostring so conic contours can be printed. --- src/python/pathops/_pathops.pxd | 2 ++ src/python/pathops/_pathops.pyx | 16 +++++++++++++++- tests/pathops_test.py | 18 +++++++++++++++--- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index cb87993..03d7406 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -135,6 +135,8 @@ cdef class Path: cpdef simplify(self, bint fix_winding=*, keep_starting_points=*) + cpdef convertConicsToQuads(self) + cpdef stroke(self, SkScalar width, LineCap cap, LineJoin join, SkScalar miter_limit) cdef list getVerbs(self) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 9f02cbf..b30ea5f 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -246,9 +246,16 @@ cdef class Path: coords_to_string = lambda fs: (", ".join("%g" % f for f in fs)) s = ["path.fillType = %s" % self.fillType] for verb, pts in self: + print(verb, [pt for pt in pts]) + for verb, pts in self: + # if the last pt isn't a pt, such as for conic weight, peel it off + suffix = '' + if pts and not isinstance(pts[-1], tuple): + suffix = "[%s]" % coords_to_string([pts[-1]]) + pts = pts[:-1] method = VERB_METHODS[verb] coords = itertools.chain(*pts) - line = "path.%s(%s)" % (method, coords_to_string(coords)) + line = "path.%s(%s)%s" % (method, coords_to_string(coords), suffix) s.append(line) return "\n".join(s) @@ -336,7 +343,11 @@ cdef class Path: if keep_starting_points: restore_starting_points(self, first_points) + cpdef convertConicsToQuads(self): + pass + cpdef stroke(self, SkScalar width, LineCap cap, LineJoin join, SkScalar miter_limit): + # Do stroke stroke_rec = new SkStrokeRec(kFill_InitStyle) try: stroke_rec.setStrokeStyle(width, False) @@ -345,6 +356,9 @@ cdef class Path: finally: del stroke_rec + # Nuke any conics that snuck in + self.convertConicsToQuads() + cdef list getVerbs(self): cdef int i, count cdef uint8_t *verbs diff --git a/tests/pathops_test.py b/tests/pathops_test.py index 8e8ba0a..71c82b7 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -699,7 +699,7 @@ def test_strip_collinear_moveTo(): ( ('moveTo', (5, 5)), ('lineTo', (10, 5)), - ('stroke', (2,)), + ('stroke', (2, 0, 0, 1)), ), ( ('moveTo', ((5., 4.),)), @@ -709,12 +709,24 @@ def test_strip_collinear_moveTo(): ('lineTo', ((5., 4.),)), ('closePath', ()), ), - ) + ), + # draw a conic and convert to quads + ( + ( + ('moveTo', (10, 10)), + ('conicTo', (20, 20, 10, 30, 5)), + ('convertConicsToQuads', ()), + ), + ( + ), + ), ] ) -def test_stroke_path(operations, expected): +def test_path_operation(operations, expected): path = Path() for op, args in operations: getattr(path, op)(*args) assert tuple(path.segments) == expected + + From 29deb9a38aff2e6205797f1546e4d779bfc5f4e8 Mon Sep 17 00:00:00 2001 From: Rod S Date: Tue, 28 Jan 2020 21:42:03 -0800 Subject: [PATCH 50/57] a failing test, pending figuring out what the right answer is :) --- tests/pathops_test.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/pathops_test.py b/tests/pathops_test.py index 71c82b7..fc4e1d5 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -692,10 +692,10 @@ def test_strip_collinear_moveTo(): @pytest.mark.parametrize( - "operations, expected", + "message, operations, expected", [ - # stroke a line 2 units wide ( + 'stroke_2_wide', ( ('moveTo', (5, 5)), ('lineTo', (10, 5)), @@ -710,23 +710,24 @@ def test_strip_collinear_moveTo(): ('closePath', ()), ), ), - # draw a conic and convert to quads ( + 'conic_2_quad', ( ('moveTo', (10, 10)), ('conicTo', (20, 20, 10, 30, 5)), ('convertConicsToQuads', ()), ), ( + 'duck' ), ), ] ) -def test_path_operation(operations, expected): +def test_path_operation(message, operations, expected): path = Path() for op, args in operations: getattr(path, op)(*args) - assert tuple(path.segments) == expected + assert tuple(path.segments) == expected, message From 7f1fd5026db335f260bffe01614f6784fbeb202e Mon Sep 17 00:00:00 2001 From: Rod S Date: Tue, 28 Jan 2020 21:43:54 -0800 Subject: [PATCH 51/57] declare ConvertConicToQuads --- src/python/pathops/_skia/core.pxd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/python/pathops/_skia/core.pxd b/src/python/pathops/_skia/core.pxd index 481b04c..105d654 100644 --- a/src/python/pathops/_skia/core.pxd +++ b/src/python/pathops/_skia/core.pxd @@ -131,6 +131,10 @@ cdef extern from * namespace "SkPath": kClose_Verb, kDone_Verb + cdef int ConvertConicToQuads(const SkPoint& p0, const SkPoint& p1, + const SkPoint& p2, SkScalar w, + SkPoint pts[], int pow2) + cdef extern from "include/core/SkRect.h": From 66c0e08744488a78c2762bbe135ee1eb094bca0e Mon Sep 17 00:00:00 2001 From: Rod S Date: Tue, 28 Jan 2020 21:45:27 -0800 Subject: [PATCH 52/57] first swing at wiring through conic conversion, not cleaned up in any way --- src/python/pathops/_pathops.pyx | 68 ++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index b30ea5f..0d30972 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -16,6 +16,7 @@ from ._skia.core cimport ( kDone_Verb, kFill_InitStyle, SK_ScalarNearlyZero, + ConvertConicToQuads, ) from ._skia.pathops cimport ( Op, @@ -343,8 +344,72 @@ cdef class Path: if keep_starting_points: restore_starting_points(self, first_points) + + def _has(self, verb): + return any(my_verb == verb for my_verb, _ in self) + + cpdef convertConicsToQuads(self): - pass + if not self._has(kConic_Verb): + return + + # TODO set pow2 to 1 if sweep <= 90 degrees + cdef pow2 = 2 + cdef count = 1 + 2 * (1< PyMem_Malloc(count * sizeof(SkPoint)) + if not quad_pts: + raise MemoryError() + cdef SkPoint *quad = quad_pts + + cdef SkPath temp + cdef SkPathFillType fillType = self.path.getFillType() + temp.setFillType(fillType) + + try: + prev = (0., 0.) + for verb, pts in self: + print('L1', verb, pts) + if verb != kConic_Verb: + if verb != kClose_Verb: + prev = pts[-1] + + # TODO cython got angry when I tried to make this a fn + if verb == kMove_Verb: + temp.moveTo(pts[0][0], pts[0][1]) + elif verb == kLine_Verb: + temp.lineTo(pts[0][0], pts[0][1]) + elif verb == kQuad_Verb: + temp.quadTo(pts[0][0], pts[0][1], + pts[1][0], pts[1][1]) + elif verb == kCubic_Verb: + temp.cubicTo(pts[0][0], pts[0][1], + pts[1][0], pts[1][1], + pts[2][0], pts[2][1]) + elif verb == kClose_Verb: + temp.close() + else: + raise UnsupportedVerbError(verb) + + continue + + num_quads = ConvertConicToQuads(SkPoint.Make(prev[0], prev[1]), + SkPoint.Make(pts[0][0], pts[0][1]), + SkPoint.Make(pts[1][0], pts[1][1]), + pts[2], quad_pts, pow2) + + for i in range(0, 2 * num_quads, 2): + temp.quadTo(quad_pts[i].x(), quad_pts[i].y(), + quad_pts[i + 1].x(), quad_pts[i + 1].y()) + + prev = pts[-2] # -1 is weight + + finally: + PyMem_Free(quad_pts) + + self.path = temp cpdef stroke(self, SkScalar width, LineCap cap, LineJoin join, SkScalar miter_limit): # Do stroke @@ -359,6 +424,7 @@ cdef class Path: # Nuke any conics that snuck in self.convertConicsToQuads() + cdef list getVerbs(self): cdef int i, count cdef uint8_t *verbs From 6c90fd4f045309ee13b019e75fa5d78e9bd0cd0a Mon Sep 17 00:00:00 2001 From: Rod S Date: Tue, 28 Jan 2020 22:59:00 -0800 Subject: [PATCH 53/57] confirm we get to the intended endpoint --- src/python/pathops/_pathops.pyx | 9 +++++++-- tests/pathops_test.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 0d30972..508f7b7 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -371,9 +371,10 @@ cdef class Path: try: prev = (0., 0.) for verb, pts in self: - print('L1', verb, pts) + print('L1', verb, 'prev', prev, 'pts', pts) if verb != kConic_Verb: if verb != kClose_Verb: + prev_verb = verb prev = pts[-1] # TODO cython got angry when I tried to make this a fn @@ -400,7 +401,11 @@ cdef class Path: SkPoint.Make(pts[1][0], pts[1][1]), pts[2], quad_pts, pow2) - for i in range(0, 2 * num_quads, 2): + # quat_pts[0] is a moveTo that may be a nop + if prev != (quad_pts[0].x(), quad_pts[0].y()): + temp.moveTo(quad_pts[0].x(), quad_pts[0].y()) + + for i in range(1, count, 2): temp.quadTo(quad_pts[i].x(), quad_pts[i].y(), quad_pts[i + 1].x(), quad_pts[i + 1].y()) diff --git a/tests/pathops_test.py b/tests/pathops_test.py index fc4e1d5..a0785eb 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -714,11 +714,13 @@ def test_strip_collinear_moveTo(): 'conic_2_quad', ( ('moveTo', (10, 10)), - ('conicTo', (20, 20, 10, 30, 5)), + ('conicTo', (20, 20, 10, 30, 3)), ('convertConicsToQuads', ()), ), ( - 'duck' + ('moveTo', ((10.0, 10.0),)), + ('qCurveTo', ((14.39, 18.79), (17.50, 26.04), (17.50, 28.96), (14.39, 30.00), (10.0, 30.0))), + ('endPath', ()) ), ), ] @@ -727,7 +729,13 @@ def test_path_operation(message, operations, expected): path = Path() for op, args in operations: getattr(path, op)(*args) - - assert tuple(path.segments) == expected, message + # 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 From 35163173d673d88f7cae299f28b6bb7e609a0351 Mon Sep 17 00:00:00 2001 From: Rod S Date: Wed, 5 Feb 2020 22:45:29 -0800 Subject: [PATCH 54/57] adjust quad building --- src/python/pathops/_pathops.pyx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 508f7b7..599d7f1 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -246,8 +246,6 @@ cdef class Path: else: coords_to_string = lambda fs: (", ".join("%g" % f for f in fs)) s = ["path.fillType = %s" % self.fillType] - for verb, pts in self: - print(verb, [pt for pt in pts]) for verb, pts in self: # if the last pt isn't a pt, such as for conic weight, peel it off suffix = '' @@ -353,8 +351,8 @@ cdef class Path: if not self._has(kConic_Verb): return - # TODO set pow2 to 1 if sweep <= 90 degrees - cdef pow2 = 2 + # TODO set pow2 to 2 if sweep > 90 degrees + cdef pow2 = 1 cdef count = 1 + 2 * (1< Date: Thu, 6 Feb 2020 16:02:44 +0000 Subject: [PATCH 55/57] expose svg-like SkPath::arcTo method as Path.arcTo() https://skia.org/user/api/SkPath_Reference#SkPath_arcTo The SkPath::arcTo method implements the functionality of SVG arcs. The only difference is that the SVG 'sweep-flag' value is opposite the integer value of 'sweep' argument in SkPath::arcTo. That is, SVG 'sweep-flag' uses 1 for clockwise, whereas SkPathDirection::kCW cast to int is 0. So in nanosvg, if we want to use this to draw SVG arc commands onto a pathops.Path, we need to make sure that we negate the sweep flag. --- src/python/pathops/__init__.py | 2 ++ src/python/pathops/_pathops.pxd | 24 ++++++++++++++++++++++++ src/python/pathops/_pathops.pyx | 13 +++++++++++++ src/python/pathops/_skia/core.pxd | 13 +++++++++++++ tests/pathops_test.py | 27 +++++++++++++++++++++++++++ 5 files changed, 79 insertions(+) diff --git a/src/python/pathops/__init__.py b/src/python/pathops/__init__.py index 9e39e59..5102972 100644 --- a/src/python/pathops/__init__.py +++ b/src/python/pathops/__init__.py @@ -6,6 +6,8 @@ FillType, LineCap, LineJoin, + ArcSize, + Direction, op, simplify, OpBuilder, diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index 03d7406..600c8de 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -12,6 +12,9 @@ from ._skia.core cimport ( kCubic_Verb, kClose_Verb, kDone_Verb, + kSmall_ArcSize, + kLarge_ArcSize, + SkPathDirection, ) from ._skia.pathops cimport ( SkOpBuilder, @@ -51,6 +54,16 @@ cpdef enum LineJoin: BEVEL_JOIN = SkLineJoin.kBevel_Join +cpdef enum ArcSize: + SMALL = kSmall_ArcSize + LARGE = kLarge_ArcSize + + +cpdef enum Direction: + CW = SkPathDirection.kCW + CCW = SkPathDirection.kCCW + + cdef union FloatIntUnion: float Float int32_t SignBitInt @@ -121,6 +134,17 @@ cdef class Path: SkScalar y3, ) + cpdef void arcTo( + self, + SkScalar rx, + SkScalar ry, + SkScalar xAxisRotate, + ArcSize largeArc, + Direction sweep, + SkScalar x, + SkScalar y, + ) + cpdef void close(self) cpdef void reset(self) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 508f7b7..a60072a 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -7,6 +7,7 @@ from ._skia.core cimport ( SkRect, SkLineCap, SkLineJoin, + SkPathDirection, kMove_Verb, kLine_Verb, kQuad_Verb, @@ -210,6 +211,18 @@ cdef class Path: ): self.path.cubicTo(x1, y1, x2, y2, x3, y3) + cpdef void arcTo( + self, + SkScalar rx, + SkScalar ry, + SkScalar xAxisRotate, + ArcSize largeArc, + Direction sweep, + SkScalar x, + SkScalar y, + ): + self.path.arcTo(rx, ry, xAxisRotate, largeArc, sweep, x, y) + cpdef void close(self): self.path.close() diff --git a/src/python/pathops/_skia/core.pxd b/src/python/pathops/_skia/core.pxd index 105d654..d688636 100644 --- a/src/python/pathops/_skia/core.pxd +++ b/src/python/pathops/_skia/core.pxd @@ -12,6 +12,10 @@ cdef extern from "include/core/SkPathTypes.h": kInverseWinding "SkPathFillType::kInverseWinding", kInverseEvenOdd "SkPathFillType::kInverseEvenOdd" + enum SkPathDirection: + kCW "SkPathDirection::kCW" + kCCW "SkPathDirection::kCCW" + cdef extern from "include/core/SkPath.h": @@ -57,6 +61,11 @@ cdef extern from "include/core/SkPath.h": SkScalar w) void conicTo(const SkPoint& p1, const SkPoint& p2, SkScalar w) + void arcTo(SkScalar rx, SkScalar ry, SkScalar xAxisRotate, ArcSize largeArc, + SkPathDirection sweep, SkScalar x, SkScalar y) + void arcTo(SkPoint& r, SkScalar xAxisRotate, ArcSize largeArc, + SkPathDirection sweep, SkPoint& xy) + void close() void dump() @@ -135,6 +144,10 @@ cdef extern from * namespace "SkPath": const SkPoint& p2, SkScalar w, SkPoint pts[], int pow2) + enum ArcSize: + kSmall_ArcSize + kLarge_ArcSize + cdef extern from "include/core/SkRect.h": diff --git a/tests/pathops_test.py b/tests/pathops_test.py index a0785eb..3c77256 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -8,6 +8,8 @@ FillType, bits2float, float2bits, + ArcSize, + Direction, ) import pytest @@ -723,6 +725,31 @@ def test_strip_collinear_moveTo(): ('endPath', ()) ), ), + ( + 'arc_to_quads', + ( + ('moveTo', (7, 5)), + ('arcTo', (3, 1, 0, ArcSize.SMALL, Direction.CCW, 7, 2)), + ('convertConicsToQuads', ()), + ), + ( + ('moveTo', ((7.0, 5.0),)), + ( + 'qCurveTo', ( + (7.9, 5.0), + (9.55, 4.77), + (10.81, 4.35), + (11.5, 3.8), + (11.5, 3.2), + (10.81, 2.65), + (9.55, 2.23), + (7.9, 2.0), + (7.0, 2.0) + ) + ), + ('endPath', ()), + ) + ) ] ) def test_path_operation(message, operations, expected): From 01906609a6ab6e7296061a8a3de2e2e617433888 Mon Sep 17 00:00:00 2001 From: Rod S Date: Mon, 10 Feb 2020 19:52:01 -0800 Subject: [PATCH 56/57] Use SkConic::computeQuadPOW2 instead of guessing --- setup.py | 3 ++- src/python/pathops/_pathops.pyx | 28 +++++++++++++++++++++------- src/python/pathops/_skia/core.pxd | 5 +++++ tests/pathops_test.py | 14 +------------- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index 7fa8527..c01cf69 100644 --- a/setup.py +++ b/setup.py @@ -276,8 +276,9 @@ def build_skia(build_base): pkg_dir = os.path.join("src", "python") cpp_dir = os.path.join("src", "cpp") skia_dir = os.path.join(cpp_dir, "skia") +skia_src_dir = os.path.join(skia_dir, "src") # allow access to internals -include_dirs = [os.path.join(skia_dir)] +include_dirs = [skia_dir, skia_src_dir] extra_compile_args = { '': [ diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 298860a..93386c1 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -1,4 +1,5 @@ from ._skia.core cimport ( + SkConic, SkPath, SkPathFillType, SkPoint, @@ -364,12 +365,12 @@ cdef class Path: if not self._has(kConic_Verb): return - # TODO set pow2 to 2 if sweep > 90 degrees - cdef pow2 = 1 - cdef count = 1 + 2 * (1< PyMem_Malloc(count * sizeof(SkPoint)) if not quad_pts: raise MemoryError() @@ -379,8 +380,12 @@ cdef class Path: cdef SkPathFillType fillType = self.path.getFillType() temp.setFillType(fillType) + cdef SkConic conic + cdef SkPoint p0 cdef SkPoint p1 cdef SkPoint p2 + cdef SkScalar weight + cdef pow2 try: prev = (0., 0.) @@ -409,10 +414,19 @@ cdef class Path: continue - num_quads = ConvertConicToQuads(SkPoint.Make(prev[0], prev[1]), - SkPoint.Make(pts[0][0], pts[0][1]), - SkPoint.Make(pts[1][0], pts[1][1]), - pts[2], quad_pts, pow2) + # Figure out a good value for pow2 + p0 = SkPoint.Make(prev[0], prev[1]) + p1 = SkPoint.Make(pts[0][0], pts[0][1]) + p2 = SkPoint.Make(pts[1][0], pts[1][1]) + weight = pts[2] + + conic.set(p0, p1, p2, weight) + # TODO is 0.25 too delicate? - blindly copies from Skias own use + pow2 = conic.computeQuadPOW2(0.25) + assert pow2 <= max_pow2 + num_quads = ConvertConicToQuads(p0, p1, p2, + weight, quad_pts, + pow2) # quad_pts[0] is effectively a moveTo that may be a nop if prev != (quad_pts[0].x(), quad_pts[0].y()): diff --git a/src/python/pathops/_skia/core.pxd b/src/python/pathops/_skia/core.pxd index d688636..5a1cf7d 100644 --- a/src/python/pathops/_skia/core.pxd +++ b/src/python/pathops/_skia/core.pxd @@ -3,6 +3,11 @@ from libc.stdint cimport uint8_t ctypedef float SkScalar +cdef extern from "core/SkGeometry.h": + cdef struct SkConic: + SkConic() + void set(const SkPoint& p0, const SkPoint& p1, const SkPoint& p2, SkScalar w) + int computeQuadPOW2(SkScalar tol) cdef extern from "include/core/SkPathTypes.h": diff --git a/tests/pathops_test.py b/tests/pathops_test.py index 3c77256..2417a95 100644 --- a/tests/pathops_test.py +++ b/tests/pathops_test.py @@ -734,19 +734,7 @@ def test_strip_collinear_moveTo(): ), ( ('moveTo', ((7.0, 5.0),)), - ( - 'qCurveTo', ( - (7.9, 5.0), - (9.55, 4.77), - (10.81, 4.35), - (11.5, 3.8), - (11.5, 3.2), - (10.81, 2.65), - (9.55, 2.23), - (7.9, 2.0), - (7.0, 2.0) - ) - ), + ('qCurveTo', ((11.5, 5.0), (11.5, 2.0), (7.0, 2.0))), ('endPath', ()), ) ) From 27f89fe7520d1a51474e4b4703198e9daf1ae205 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Sat, 15 Feb 2020 16:27:02 +0000 Subject: [PATCH 57/57] make convertConicsToQuads accept a custom tolerance argument --- src/python/pathops/_pathops.pxd | 2 +- src/python/pathops/_pathops.pyx | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/python/pathops/_pathops.pxd b/src/python/pathops/_pathops.pxd index 600c8de..74b66c0 100644 --- a/src/python/pathops/_pathops.pxd +++ b/src/python/pathops/_pathops.pxd @@ -159,7 +159,7 @@ cdef class Path: cpdef simplify(self, bint fix_winding=*, keep_starting_points=*) - cpdef convertConicsToQuads(self) + cpdef convertConicsToQuads(self, float tolerance=*) cpdef stroke(self, SkScalar width, LineCap cap, LineJoin join, SkScalar miter_limit) diff --git a/src/python/pathops/_pathops.pyx b/src/python/pathops/_pathops.pyx index 93386c1..f0bb562 100644 --- a/src/python/pathops/_pathops.pyx +++ b/src/python/pathops/_pathops.pyx @@ -360,8 +360,8 @@ cdef class Path: def _has(self, verb): return any(my_verb == verb for my_verb, _ in self) - - cpdef convertConicsToQuads(self): + cpdef convertConicsToQuads(self, float tolerance=0.25): + # TODO is 0.25 too delicate? - blindly copies from Skias own use if not self._has(kConic_Verb): return @@ -421,8 +421,7 @@ cdef class Path: weight = pts[2] conic.set(p0, p1, p2, weight) - # TODO is 0.25 too delicate? - blindly copies from Skias own use - pow2 = conic.computeQuadPOW2(0.25) + pow2 = conic.computeQuadPOW2(tolerance) assert pow2 <= max_pow2 num_quads = ConvertConicToQuads(p0, p1, p2, weight, quad_pts,