python-for-android icon indicating copy to clipboard operation
python-for-android copied to clipboard

Pillow-SIMD recipe?

Open podraco opened this issue 3 years ago • 4 comments

Greetings i recently am testing some optimizations for my projects and i'm stuck trying to replace the normal pillow with pillow-SIMD for an android app. Supposedly, pillow-simd is a drop and replace lib for pillow 7.0.0 but i'm stuck trying to perform the recipe for the lib.

the recipe should be a fork like from the current pillow recipe, as far as i understand. yet, i'm stuck with this error:

myapp/PIL/Image.py:90: RuntimeWarning: The _imaging extension was built for another version of Pillow or PIL:

here are the files i'm using. pythonforandroid/recipes/pillow-simd/__init__.py

from os.path import join

from pythonforandroid.recipe import CompiledComponentsPythonRecipe

class Pillow_SimdRecipe(CompiledComponentsPythonRecipe):
    """
    A recipe for Pillow (previously known as Pil).

    This recipe allow us to build the Pillow recipe with support for different
    types of images and fonts. But you should be aware, that in order to  use
    some of the features of  Pillow, we must build some libraries. By default
    we automatically trigger the build of below libraries::

        - freetype: rendering fonts support.
        - harfbuzz: a text shaping library.
        - jpeg: reading and writing JPEG image files.
        - png: support for PNG images.

    But you also could enable the build of some extra image types by requesting
    the build of some libraries via argument `requirements`::

        - libwebp: library to encode and decode images in WebP format.
    """

    version = 'master'
    url = 'https://github.com/uploadcare/pillow-simd/archive/simd/{version}.zip'
    site_packages_name = 'Pillow'
    depends = ['png', 'jpeg', 'freetype', 'setuptools']
    opt_depends = ['libwebp']
    patches = [join('patches', 'fix-setup.patch')]
    conflicts = ['pillow']

    call_hostpython_via_targetpython = False
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
        env = super().get_recipe_env(arch, with_flags_in_cc)

        env['ANDROID_ROOT'] = join(self.ctx.ndk_platform, 'usr')
        ndk_lib_dir = join(self.ctx.ndk_platform, 'usr', 'lib')
        ndk_include_dir = join(self.ctx.ndk_dir, 'sysroot', 'usr', 'include')

        png = self.get_recipe('png', self.ctx)
        png_lib_dir = join(png.get_build_dir(arch.arch), '.libs')
        png_inc_dir = png.get_build_dir(arch)

        jpeg = self.get_recipe('jpeg', self.ctx)
        jpeg_inc_dir = jpeg_lib_dir = jpeg.get_build_dir(arch.arch)

        freetype = self.get_recipe('freetype', self.ctx)
        free_lib_dir = join(freetype.get_build_dir(arch.arch), 'objs', '.libs')
        free_inc_dir = join(freetype.get_build_dir(arch.arch), 'include')

        # harfbuzz is a direct dependency of freetype and we need the proper
        # flags to successfully build the Pillow recipe, so we add them here.
        harfbuzz = self.get_recipe('harfbuzz', self.ctx)
        harf_lib_dir = join(harfbuzz.get_build_dir(arch.arch), 'src', '.libs')
        harf_inc_dir = harfbuzz.get_build_dir(arch.arch)

        # these below env variables are defined at Pillow's `setup.py`
        env['JPEG_ROOT'] = f'{jpeg_lib_dir}|{jpeg_inc_dir}'
        env['FREETYPE_ROOT'] = f'{free_lib_dir}|{free_inc_dir}'
        env['ZLIB_ROOT'] = f'{ndk_lib_dir}|{ndk_include_dir}'

        # libwebp is an optional dependency, so we add the
        # flags if we have it in our `ctx.recipe_build_order`
        build_with_webp_support = 'libwebp' in self.ctx.recipe_build_order
        if build_with_webp_support:
            webp = self.get_recipe('libwebp', self.ctx)
            webp_install = join(
                webp.get_build_dir(arch.arch), 'installation'
            )

        # Add libraries includes to CFLAGS
        cflags = f' -I{png_inc_dir}'
        cflags += f' -I{harf_inc_dir} -I{join(harf_inc_dir, "src")}'
        cflags += f' -I{free_inc_dir}'
        cflags += f' -I{jpeg_inc_dir}'
        if build_with_webp_support:
            cflags += f' -I{join(webp_install, "include")}'
        cflags += f' -I{ndk_include_dir}'

        # Link the basic Pillow libraries...no need to add webp's libraries
        # since it seems that the linkage is properly made without it :)
        env['LIBS'] = ' -lpng -lfreetype -lharfbuzz -ljpeg -lturbojpeg'

        # Add libraries locations to LDFLAGS
        env['LDFLAGS'] += f' -L{png_lib_dir}'
        env['LDFLAGS'] += f' -L{harf_lib_dir}'
        env['LDFLAGS'] += f' -L{jpeg_lib_dir}'
        if build_with_webp_support:
            env['LDFLAGS'] += f' -L{join(webp_install, "lib")}'
        env['LDFLAGS'] += f' -L{ndk_lib_dir}'
        if cflags not in env['CFLAGS']:
            env['CFLAGS'] += cflags + "cc -mavx2"
        return env


recipe = Pillow_SimdRecipe()

pythonforandroid/recipes/pillow-simd/patches/fix-setup.patch

--- Pillow-sidm/setup.py.orig	2020-01-02 06:19:26.000000000 +0100
+++ Pillow-simd/setup.py	2020-07-05 12:26:18.882948858 +0200
@@ -29,13 +29,13 @@ def get_version():

 NAME = "Pillow-SIMD"
 PILLOW_VERSION = get_version()
-FREETYPE_ROOT = None
+FREETYPE_ROOT = tuple(os.environ['FREETYPE_ROOT'].split('|')) if 'FREETYPE_ROOT' in os.environ else None
 IMAGEQUANT_ROOT = None
 JPEG2K_ROOT = None
-JPEG_ROOT = None
+JPEG_ROOT = tuple(os.environ['JPEG_ROOT'].split('|')) if 'JPEG_ROOT' in os.environ else None
 LCMS_ROOT = None
 TIFF_ROOT = None
-ZLIB_ROOT = None
+ZLIB_ROOT = tuple(os.environ['ZLIB_ROOT'].split('|')) if 'ZLIB_ROOT' in os.environ else None


 if sys.platform == "win32" and sys.version_info >= (3, 9):
@@ -317,7 +317,7 @@ class pil_build_ext(build_ext):
     )

     def initialize_options(self):
-        self.disable_platform_guessing = None
+        self.disable_platform_guessing = True
         self.add_imaging_libs = ""
         build_ext.initialize_options(self)
         for x in self.feature:
@@ -567,62 +567,6 @@ class pil_build_ext(build_ext):
                     feature.jpeg = "libjpeg"  # alternative name

         feature.openjpeg_version = None
-        if feature.want("jpeg2000"):
-            _dbg("Looking for jpeg2000")
-            best_version = None
-            best_path = None
-
-            # Find the best version
-            for directory in self.compiler.include_dirs:
-                _dbg("Checking for openjpeg-#.# in %s", directory)
-                try:
-                    listdir = os.listdir(directory)
-                except Exception:
-                    # WindowsError, FileNotFoundError
-                    continue
-                for name in listdir:
-                    if name.startswith("openjpeg-") and os.path.isfile(
-                        os.path.join(directory, name, "openjpeg.h")
-                    ):
-                        _dbg("Found openjpeg.h in %s/%s", (directory, name))
-                        version = tuple(int(x) for x in name[9:].split("."))
-                        if best_version is None or version > best_version:
-                            best_version = version
-                            best_path = os.path.join(directory, name)
-                            _dbg(
-                                "Best openjpeg version %s so far in %s",
-                                (best_version, best_path),
-                            )
-
-            if best_version and _find_library_file(self, "openjp2"):
-                # Add the directory to the include path so we can include
-                # <openjpeg.h> rather than having to cope with the versioned
-                # include path
-                # FIXME (melvyn-sopacua):
-                # At this point it's possible that best_path is already in
-                # self.compiler.include_dirs. Should investigate how that is
-                # possible.
-                _add_directory(self.compiler.include_dirs, best_path, 0)
-                feature.jpeg2000 = "openjp2"
-                feature.openjpeg_version = ".".join(str(x) for x in best_version)
-
-        if feature.want("imagequant"):
-            _dbg("Looking for imagequant")
-            if _find_include_file(self, "libimagequant.h"):
-                if _find_library_file(self, "imagequant"):
-                    feature.imagequant = "imagequant"
-                elif _find_library_file(self, "libimagequant"):
-                    feature.imagequant = "libimagequant"
-
-        if feature.want("tiff"):
-            _dbg("Looking for tiff")
-            if _find_include_file(self, "tiff.h"):
-                if _find_library_file(self, "tiff"):
-                    feature.tiff = "tiff"
-                if sys.platform in ["win32", "darwin"] and _find_library_file(
-                    self, "libtiff"
-                ):
-                    feature.tiff = "libtiff"

         if feature.want("freetype"):
             _dbg("Looking for freetype")
@@ -647,15 +591,6 @@ class pil_build_ext(build_ext):
                     if subdir:
                         _add_directory(self.compiler.include_dirs, subdir, 0)

-        if feature.want("lcms"):
-            _dbg("Looking for lcms")
-            if _find_include_file(self, "lcms2.h"):
-                if _find_library_file(self, "lcms2"):
-                    feature.lcms = "lcms2"
-                elif _find_library_file(self, "lcms2_static"):
-                    # alternate Windows name.
-                    feature.lcms = "lcms2_static"
-
         if feature.want("webp"):
             _dbg("Looking for webp")
             if _find_include_file(self, "webp/encode.h") and _find_include_file(
@@ -717,8 +652,8 @@ class pil_build_ext(build_ext):
             defs.append(("HAVE_LIBTIFF", None))
         if sys.platform == "win32":
             libs.extend(["kernel32", "user32", "gdi32"])
-        if struct.unpack("h", b"\0\1")[0] == 1:
-            defs.append(("WORDS_BIGENDIAN", None))
+        # if struct.unpack("h", b"\0\1")[0] == 1:
+        #     defs.append(("WORDS_BIGENDIAN", None))

         if sys.platform == "win32" and not (PLATFORM_PYPY or PLATFORM_MINGW):
             defs.append(("PILLOW_VERSION", '"\\"%s\\""' % PILLOW_VERSION))
@@ -769,16 +704,6 @@ class pil_build_ext(build_ext):
                 )
             )

-        tk_libs = ["psapi"] if sys.platform == "win32" else []
-        exts.append(
-            Extension(
-                "PIL._imagingtk",
-                ["src/_imagingtk.c", "src/Tk/tkImaging.c"],
-                include_dirs=["src/Tk"],
-                libraries=tk_libs,
-            )
-        )
-
         exts.append(Extension("PIL._imagingmath", ["src/_imagingmath.c"]))
         exts.append(Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]))

podraco avatar Feb 14 '21 21:02 podraco

Are you sure pillow-simd is going to be compatible with arm? https://github.com/uploadcare/pillow-simd#why-do-not-contribute-simd-to-the-original-pillow seems to indicate it depends on x86 (and related) specific instructions, i'm not sure SSE4 and AVX2 have equivalent instructions set or armv8, and it's likely that it won't "just work", even if they do. unless you are targeting android x86, then it might work indeed, but that's a tiny subset of android devices, and not all of them might support this recent instructions sets.

references: https://en.wikipedia.org/wiki/SSE4 https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#Advanced_Vector_Extensions_2

tshirtman avatar Feb 14 '21 21:02 tshirtman

You're right it was a nice experiment that i wanted to test since i got an aarch64 (arm64-v8a). https://developer.android.com/ndk/guides/cpu-arm-neon?hl=es-419

podraco avatar Feb 14 '21 22:02 podraco

Ah, indeed, Neon would be a way, though it doesn't seem like pillow-simd supports it at the moment, as https://github.com/uploadcare/pillow-simd/issues/8#issuecomment-404089929 and https://github.com/uploadcare/pillow-simd/issues/3 are the only places in the project that seems to mention it (aside the readme), so unless you plan to add this support yourself, (which i'm sure they would appreciate, but it certainly a very much sizeable effort), i don't think it will help at the moment.

tshirtman avatar Feb 14 '21 22:02 tshirtman

Don't worry, it resolved my question about if it was a python for android incompatibility or the project itself. Thanks for your time bro.

podraco avatar Feb 15 '21 04:02 podraco