PythonのimagecodecsをAWS Lambda Layers用にビルド（あるいはTIFF地獄）

# TIFF地獄とは

TIFFは暗黒大陸とも言われるほど（言われてない）広大な規格である。いま存在する処理系はどれも、規格のごく一部を実装したものにすぎない。
広大な規格であるがゆえに、そのごく一部を実装するだけでも、莫大な数のライブラリをリンクする必要がある。
2019年1月現在、Christoph Gohlkeという冒険家が、この暗黒大陸に挑戦している。彼が進めているプロジェクトが、[tifffile](https://pypi.org/project/tifffile/)と[imagecodecs](https://pypi.org/project/imagecodecs/)である。
imagecodecsは、「OSSの画像コーデックライブラリをすべて集めてPythonで使えるようにしよう」という壮大なものらしい。
必然的に、AWS Lambdaのような古い環境では、ビルドが非常に面倒である。しかしimagecodecsを避けると、失う面積は大きい。

# 準備

要Docker。

### Numpy / SciPy

AWS公式のNumpy・SciPyレイヤは、PyPIから取れるwhlと互換性がない。おそらくビルド時にいろいろ最適化してある。公式のレイヤをダウンロードする方法はないようなので、諦めてPyPIのwhlをレイヤに固める。

追記：AWS Lambdaの展開後250MB制限がきついので、節約術を使った。`strip -s`と、共有ライブラリ（gfortran、OpenBLAS）をシンボリックリンクでひとつにまとめた。60MBほどの節約になっている。

```Dockerfile:Dockerfile
FROM lambci/lambda:build-python3.6
ENV AWS_DEFAULT_REGION ap-northeast-1

ENV HOME /home/hoge
RUN mkdir $HOME
WORKDIR $HOME

RUN pip install --upgrade pip
RUN pip install numpy==1.16.1 scipy==1.2.0 -t ~/python/lib/python3.6/site-packages/
RUN find ~/python/lib/python3.6/site-packages/ -name *.so|xargs -n 1 strip -s
ENV SITE_PACKAGES $HOME/python/lib/python3.6/site-packages/
RUN rm $SITE_PACKAGES/scipy/.libs/libopenblasp-r0-8dca6697.3.0.dev.so $SITE_PACKAGES/scipy/.libs/libgfortran-ed201abd.so.3.0.0
RUN ln -s  $SITE_PACKAGES/numpy/.libs/libopenblasp-r0-382c8f3a.3.5.dev.so $SITE_PACKAGES/scipy/.libs/libopenblasp-r0-8dca6697.3.0.dev.so
RUN ln -s $SITE_PACKAGES/numpy/.libs/libgfortran-ed201abd.so.3.0.0 $SITE_PACKAGES/scipy/.libs/libgfortran-ed201abd.so.3.0.0
CMD zip -ry9 ~/scipy_layer.zip python && cp ~/scipy_layer.zip /share
```

```yml:docker-compose.yml
version: '2'
services:
  app:
    build:
      context: .
    volumes:
      - .:/share
```
```bash
docker-compose build
docker-compose up
```

できたscipy_layer.zipをAWS Lambdaにレイヤとして登録しておく。


### ライブラリのソース

以下のライブラリのソースをダウンロードして、ワークディレクトリに置く。yumで入るものでは古すぎるため。

- blosc
- libpng
- libjpeg-turbo
- zlib
- libwebp

imagecodecsもソースが必要。

```bash
pip download --no-binary :all: imagecodecs
```

これ↑だとnumpyも一緒にダウンロードされてしまうが、防ぐ方法が見当たらない。

### imagecodecsのsetup.pyを編集

AWS Lambdaのzlibとlibjpeg-turboが古いので、静的リンクする必要がある。extra_objectsのところを加える。

```python:setup.py（一部）
ext_modules = [
    Extension(
        'imagecodecs._imagecodecs',
        sources,
        include_dirs=include_dirs,
        libraries=libraries,
        define_macros=define_macros,
        extra_objects=['/home/hoge/foo/lib/libz.a',
            '/home/hoge/foo/lib64/libturbojpeg.a',
            '/home/hoge/foo/lib64/libjpeg.a'],
    )
]
```

編集したsetup.pyはワークディレクトリに置く。

```Dockerfile:Dockerfile
FROM lambci/lambda:build-python3.6
ENV AWS_DEFAULT_REGION ap-northeast-1

ENV HOME /home/hoge
RUN mkdir $HOME
WORKDIR $HOME

COPY c-blosc-master.zip .
RUN unzip c-blosc-master.zip
COPY libpng-libpng16.zip .
RUN unzip libpng-libpng16.zip
COPY libjpeg-turbo-master.zip .
RUN unzip libjpeg-turbo-master.zip
COPY zlib-1.2.11.tar.gz .
RUN tar zxf zlib-1.2.11.tar.gz
COPY libwebp-1.0.1.zip .
RUN unzip libwebp-1.0.1.zip

RUN yum update -y
RUN sed -i -e 's/enabled=0/enabled=1/' /etc/yum.repos.d/epel.repo
RUN yum install -y cmake3 openjpeg2-devel libzstd-devel lz4-devel jxrlib-devel nasm
RUN pip install --upgrade pip
RUN pip install numpy==1.16.0
RUN mkdir foo

RUN mkdir ~/c-blosc-master/build
WORKDIR $HOME/c-blosc-master/build
RUN cmake3 -D CMAKE_INSTALL_PREFIX=$HOME/foo ..
RUN make install

RUN mkdir ~/libpng-libpng16/build
WORKDIR $HOME/libpng-libpng16/build
RUN cmake3 -D CMAKE_INSTALL_PREFIX=$HOME/foo ..
RUN make install

RUN mkdir ~/libjpeg-turbo-master/build
WORKDIR $HOME/libjpeg-turbo-master/build
RUN cmake3 -D CMAKE_INSTALL_PREFIX=$HOME/foo -D CMAKE_POSITION_INDEPENDENT_CODE=ON ..
RUN make install

WORKDIR $HOME/zlib-1.2.11
RUN CFLAGS='-mstackrealign -fPIC -O3' ./configure --prefix=$HOME/foo
RUN make install

RUN mkdir ~/libwebp-1.0.1/build
WORKDIR $HOME/libwebp-1.0.1/build
RUN cmake3 -D CMAKE_INSTALL_PREFIX=$HOME/foo ..
RUN make install

ENV LD_LIBRARY_PATH $LD_LIBRARY_PATH:$HOME/foo/lib:$HOME/foo/lib64
ENV CPATH $HOME/foo/include
ENV LIBRARY_PATH $HOME/foo/lib:$HOME/foo/lib64

WORKDIR $HOME
COPY imagecodecs-2019.1.1.tar.gz .
RUN tar zxf imagecodecs-2019.1.1.tar.gz
COPY setup.py $HOME/imagecodecs-2019.1.1
WORKDIR $HOME/imagecodecs-2019.1.1
RUN mkdir ~/bar
RUN pip install . -t ~/bar

RUN mkdir -p ~/fuga/python/lib/python3.6/site-packages
RUN mv ~/bar/imagecodecs* ~/fuga/python/lib/python3.6/site-packages
RUN mkdir ~/fuga/lib
RUN cp -d /usr/lib64/libopenjp2.so* /usr/lib64/libzstd.so* /usr/lib64/liblz4.so* \
    /usr/lib64/libjpegxr.so* \
    /usr/lib64/libjxrglue.so* ~/foo/lib/*.so* \
    ~/foo/lib64/*.so* ~/fuga/lib/

WORKDIR $HOME/fuga
CMD zip -ry9 ~/imagecodecs_layer.zip * && cp ~/imagecodecs_layer.zip /share
```

上記のDockerfileと、さきほどと同じdocker-compose.ymlを、ワークディレクトリに置く。

# ビルド

ワークディレクトリには以下のファイルがあるものとする。

```bash
$ ls
c-blosc-master.zip  Dockerfile             imagecodecs-2019.1.1.tar.gz  libpng-libpng16.zip  setup.py
docker-compose.yml  imagecodecs_layer.zip  libjpeg-turbo-master.zip     libwebp-1.0.1.zip    zlib-1.2.11.tar.gz
```

もしファイル名が違う場合はDockerfileを編集。

```bash
docker-compose build
docker-compose up
```
ワークディレクトリにimagecodecs_layer.zipができる。

# import

imagecodecsをインポートする前に、レイヤ内の共有ライブラリをロードする。こうしないと共有ライブラリが見つからずにエラーになる。

```python
import ctypes
ctypes.CDLL('/opt/lib/liblz4.so.1')
ctypes.CDLL('/opt/lib/libzstd.so.1')
ctypes.CDLL('/opt/lib/libpng16.so.16')
ctypes.CDLL('/opt/lib/libblosc.so.1')
ctypes.CDLL('/opt/lib/libopenjp2.so.7')
ctypes.CDLL('/opt/lib/libjpegxr.so.0')
ctypes.CDLL('/opt/lib/libjxrglue.so.0')
import imagecodecs
```

これらも静的リンクするほうがいいかも。