lambda で heic 形式の画像を扱うためにやったこと

## 背景

iphone のカメラで保存された画像はデフォで heic という形式になっており、web ブラウザは現状解釈してくれない。そのため、他のjpgなどの画像と同じように扱うためにはサーバ側で加工する必要がある。[^1] 今回は、サーバ側でどうにかする手段をやってみたので残しとこうと思う。

## 内容

### 結論

lambda に imagemagick を仕込んだ lambda layer を連携させて、imagemagick を叩けばok

### 実装方法

lambda の runtime は node v12
lambda の管理には serverless framework
lambda layer はこの[リポジトリ](https://github.com/serverlesspub/imagemagick-aws-lambda-2)のものを使う。ただ、 heic を扱うためのライブラリが入っていないので同リポジトリに上がっていたPRからビルド手段を持ってきました。imagemagick のビルドフラグで heic を追加したり、必要なモジュールである、libheif, libde265 をビルドできるようにしている。

```Makefile
LIBPNG_VERSION ?= 1.6.37
LIBJPG_VERSION ?= 9c
OPENJP2_VERSION ?= 2.3.1
LIBTIFF_VERSION ?= 4.0.9
BZIP2_VERSION ?= 1.0.6
LIBWEBP_VERSION ?= 0.6.1
IMAGEMAGICK_VERSION ?= 7.0.8-68
LIBHEIF_VERSION ?= 1.6.0
LIBDE265_VERSION ?= 1.0.3

TARGET_DIR ?= /opt/
PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
CACHE_DIR=$(PROJECT_ROOT)build/cache

.ONESHELL:

CONFIGURE = PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig \
	./configure \
		CPPFLAGS=-I$(CACHE_DIR)/include \
		LDFLAGS=-L$(CACHE_DIR)/lib \
		--disable-dependency-tracking \
		--disable-shared \
		--enable-static \
		--prefix=$(CACHE_DIR)

## libde265
LIBDE265_SOURCE=libde265-$(LIBDE265_VERSION).tar.gz

$(LIBDE265_SOURCE):
	curl -LO https://github.com/strukturag/libde265/releases/download/v$(LIBDE265_VERSION)/$(LIBDE265_SOURCE)

$(CACHE_DIR)/lib/libde265.a: $(LIBDE265_SOURCE)
	tar xf $<
	cd libde265*
	./autogen.sh
	$(CONFIGURE)
	make
	make install

## libheic
LIBHEIF_SOURCE=libheif-$(LIBHEIF_VERSION).tar.gz

$(LIBHEIF_SOURCE):
	curl -LO https://github.com/strukturag/libheif/releases/download/v$(LIBHEIF_VERSION)/$(LIBHEIF_SOURCE)

$(CACHE_DIR)/lib/libheif.a: $(LIBHEIF_SOURCE)
	tar xf $<
	cd libheif*
	./autogen.sh
	$(CONFIGURE)
	make
	make install


## libjpg

LIBJPG_SOURCE=jpegsrc.v$(LIBJPG_VERSION).tar.gz

$(LIBJPG_SOURCE):
	curl -LO http://ijg.org/files/$(LIBJPG_SOURCE)

$(CACHE_DIR)/lib/libjpeg.a: $(LIBJPG_SOURCE)
	tar xf $<
	cd jpeg*
	$(CONFIGURE)	 
	make
	make install


## libpng

LIBPNG_SOURCE=libpng-$(LIBPNG_VERSION).tar.xz

$(LIBPNG_SOURCE):
	curl -LO http://prdownloads.sourceforge.net/libpng/$(LIBPNG_SOURCE)

$(CACHE_DIR)/lib/libpng.a: $(LIBPNG_SOURCE)
	tar xf $<
	cd libpng*
	$(CONFIGURE)	 
	make
	make install

# libbz2

BZIP2_SOURCE=bzip2-$(BZIP2_VERSION).tar.gz

$(BZIP2_SOURCE):
	curl -LO http://prdownloads.sourceforge.net/bzip2/bzip2-$(BZIP2_VERSION).tar.gz

$(CACHE_DIR)/lib/libbz2.a: $(BZIP2_SOURCE)
	tar xf $<
	cd bzip2-*
	make libbz2.a
	make install PREFIX=$(CACHE_DIR)

# libtiff

LIBTIFF_SOURCE=tiff-$(LIBTIFF_VERSION).tar.gz

$(LIBTIFF_SOURCE):
	curl -LO http://download.osgeo.org/libtiff/$(LIBTIFF_SOURCE)

$(CACHE_DIR)/lib/libtiff.a: $(LIBTIFF_SOURCE) $(CACHE_DIR)/lib/libjpeg.a
	tar xf $<
	cd tiff-*
	$(CONFIGURE)	 
	make
	make install

# libwebp

LIBWEBP_SOURCE=libwebp-$(LIBWEBP_VERSION).tar.gz

$(LIBWEBP_SOURCE):
	curl -L https://github.com/webmproject/libwebp/archive/v$(LIBWEBP_VERSION).tar.gz -o $(LIBWEBP_SOURCE)
	
$(CACHE_DIR)/lib/libwebp.a: $(LIBWEBP_SOURCE)
	tar xf $<
	cd libwebp-*
	sh autogen.sh
	$(CONFIGURE)	 
	make
	make install

## libopenjp2

OPENJP2_SOURCE=openjp2-$(OPENJP2_VERSION).tar.gz

$(OPENJP2_SOURCE):
	curl -L https://github.com/uclouvain/openjpeg/archive/v$(OPENJP2_VERSION).tar.gz -o $(OPENJP2_SOURCE)


$(CACHE_DIR)/lib/libopenjp2.a: $(OPENJP2_SOURCE) $(CACHE_DIR)/lib/libpng.a $(CACHE_DIR)/lib/libtiff.a
	tar xf $<
	cd openjpeg-*
	mkdir -p build
	cd build 
	PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig cmake .. \
		-DCMAKE_BUILD_TYPE=Release \
		-DCMAKE_INSTALL_PREFIX=$(CACHE_DIR) \
		-DBUILD_SHARED_LIBS:bool=off \
		-DBUILD_CODEC:bool=off
	make clean
	make install


## ImageMagick

IMAGE_MAGICK_SOURCE=ImageMagick-$(IMAGEMAGICK_VERSION).tar.gz

$(IMAGE_MAGICK_SOURCE):
	curl -L https://github.com/ImageMagick/ImageMagick/archive/$(IMAGEMAGICK_VERSION).tar.gz -o $(IMAGE_MAGICK_SOURCE)


LIBS:=$(CACHE_DIR)/lib/libjpeg.a \
	$(CACHE_DIR)/lib/libpng.a \
	$(CACHE_DIR)/lib/libopenjp2.a \
	$(CACHE_DIR)/lib/libtiff.a \
	$(CACHE_DIR)/lib/libbz2.a \
	$(CACHE_DIR)/lib/libwebp.a \
	$(CACHE_DIR)/lib/libde265.a \
	$(CACHE_DIR)/lib/libheif.a \
	$(CACHE_DIR)/lib/libwebp.a

$(TARGET_DIR)/bin/identify: $(IMAGE_MAGICK_SOURCE) $(LIBS)
	tar xf $<
	cd ImageMa*
	PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig \
		./configure \
		CPPFLAGS=-I$(CACHE_DIR)/include \
		LDFLAGS="-L$(CACHE_DIR)/lib -lstdc++" \
		--disable-dependency-tracking \
		--disable-shared \
		--enable-static \
		--with-heic=yes \
		--prefix=$(TARGET_DIR) \
		--enable-delegate-build \
		--without-modules \
		--disable-docs \
		--without-magick-plus-plus \
		--without-perl \
		--without-x \
		--disable-openmp
	make clean
	make all
	make install

libs: $(LIBS)

all: $(TARGET_DIR)/bin/identify
```

リポジトリ内にあるMakefileをみてもらえればわかるが、これらのビルド処理はlambda 実行マシンに近いdocker image 内で実行されるのでlinux 環境は必要ない。
`make all` ですべてが解決する。lambda layer を登録したらその `arn` を serverless.yml に追記して変更を反映する。

```yml
functions:
  preview-img:
    runtime: nodejs12.x
    handler: bin/index.preview
    memorySize: 1536
    layers:
      - arn:aws:lambda:ap-northeast-1:xxxxxxxxx:layer:image-magick:4
    events:
      - http:
          path: preview
          method: post
          cors:
            origin: "*"
            maxAge: 86400
```

api gateway も一応画像の content-type を絞っておく

```yml
provider:
  #
  # 
  # 
  apiGateway:
    minimumCompressionSize: 0 # gzip で返却できるように
    binaryMediaTypes:
      - image/png
      - image/jpeg
      - image/jpg
      - image/heic
```

ここまで設定が終わればあとは処理を書くだけ。node は不慣れなので書き方が雑ですがこんな風にやった。
lambda の中でファイル書き込みを行いときは `/tmp` に書くというのがお作法のようでそれに習っている。

```js
const { execSync } = require('child_process');
const fs = require('fs');

module.exports.preview = (event, context, callback) => {
    buf = new Buffer(event.body, 'base64')

    heicName = '/tmp/' + context.awsRequestId + '.heic'
    jpgName = '/tmp/' + context.awsRequestId + '.jpg'
    console.time('writeFileSync');
    fs.writeFileSync(heicName, buf, function (err) {
        console.log(`err: ${err}`);
        if (err) throw err;
    });
    console.timeEnd('writeFileSync');

    cmd = '/opt/bin/convert' + ' ' + heicName + ' ' + jpgName
    console.log(`cmd: ${cmd}`);

    console.time('convert');
    try {
        stdout = execSync(cmd);
        console.log(`cmd stdout: ${stdout.toString()}`)
    } catch (err) {
        console.log(`cmd err: ${err}`)
    }
    console.timeEnd('convert');

    stdout = execSync('ls -la /tmp')
    console.log(`ls -la: ${stdout.toString()}`)

    // return response
};
```


### その他学んだこと

ほんとは go でやりたかったけど、lambda で使われるマシンが Amazon Linux2 ではないのでダメだった。node10以降, python3 系は対応している。

他にも便利そうなlambda layer は [awesome](https://github.com/mthenw/awesome-layers) にまとまってる。ffmpeg とかもある。

memorySize: 1536 を振っていても、2~3 MB くらいの画像だと1.5秒かかるのでもうちょい早くならんのかなぁというところ

**免責**
記事内容の運用により派生した損害を含むあらゆる結果については一切の責任を持ちません。


[^1]: もしかしたら、この [ライブラリ](https://github.com/alexcorvi/heic2any) を使えばクライアントサイドでどうにかなるので不要かもしれない