HEIC（HEIF）画像をJPEGに一括変換する

# はじめに
　iOS（11以降）では写真を撮影したときに、特に設定をしていなければHEIFと呼ばれる方式で画像を保存します。拡張子はHEICです。この方式は高画質かつファイルサイズが小さいので便利そうなのですが、Apple系のOS以外ではまだサポートが充実していません。今回はHEIC画像を一括（バッチ）でJPEGに変換する方法をいくつか試してみます。

 # Windows版ImageMagickを使う
 　ImageMagickは様々なフォーマットに対応したツールで、最近HEICを読み込めるようになりました。Windowsバッチで次のように書くと、batファイルを設置した場所と同じ階層のHEICファイルを全てJEPGに変換できます。PATHを通すことができない場合はImageMagickの場所をフルパスで書くことで実行できます。

  ```bat
for %%f in (*.HEIC) do (
    start convert.exe "%%~nf".HEIC "%%~nf".JPEG
)
  ```
この例はマルチプロセスで、処理するファイル数と同じ数のプロセスが実行されます。同時に処理するファイルの数が多すぎない条件でこの方法はうまく動きます。PCのスペックによっては逐次処理に変えても良いかもしれません。

# AWS Lambdaで変換を行う
　HEICをJPEGに変換するのは1枚でも少し時間がかかります。処理するデバイスのスペックが良くないと長い時間待つことになります。手持ちのデバイスでImageMagickが必ず使えるとも限りません。AWSのS3にHEICをアップロードしたらJPEGでダウンロードできるようにしたいと思います。
　
 ## AWS LambdaでImageMagick（HEIC対応）を使えるようにする
 　ImageMagick（HEIC対応）が動くコンテナイメージを作成してLmabdaにデプロイします。現時点（2024/4/25）で動作確認をしたDockerfileとapp.pyです。同様の作業をしている方が過去にも多数いらっしゃいましたが、ImageMagickのビルド手順は頻繁にメンテしていないと動かなくなってしまうようです。

  ```Dockerfile
# Dockerfile
FROM public.ecr.aws/lambda/python:3.8

RUN yum -y install git autoconf libtool make pkgconfig gcc-c++ wget zlib-devel libpng-devel libjpeg-devel libtiff-devel

RUN cd && wget https://cmake.org/files/v3.29/cmake-3.29.0.tar.gz && tar -xvzf cmake-3.29.0.tar.gz && cd cmake-3.29.0 && \
./bootstrap -- -DCMAKE_USE_OPENSSL=OFF && make -j 32 && make install && cd

RUN cd && git clone https://github.com/strukturag/libde265 && cd libde265 && ./autogen.sh && ./configure && make -j 32 && make install && cd

RUN cd && git clone https://github.com/strukturag/libheif && cd libheif && mkdir build && cd build && cmake --preset=release .. && \
make -j 32 && make install

ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig:$PKG_CONFIG_PATH
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:$LD_LIBRARY_PATH

RUN cd && wget https://imagemagick.org/download/ImageMagick-7.1.1-31.tar.gz && tar -xzf ImageMagick-7.1.1-31.tar.gz && \
cd ImageMagick-7.1.1-31 && ./configure --enable-shared=no --enable-static=yes && make -j 32 && make install

COPY app.py ${LAMBDA_TASK_ROOT}

CMD [ "app.lambda_handler" ]

  ```

  ```py
# app.py
import json
import subprocess
import boto3
import os
import urllib.parse
import shutil
import re

s3 = boto3.resource('s3')
output_bucket = 'to-jpg-dst'
out_ext = '.jpg'

def lambda_handler(event, context):
    if 'Records' in event.keys():
        input_bucket = event['Records'][0]['s3']['bucket']['name']
        input_key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
        in_bucket = s3.Bucket(input_bucket)
    else :
        print("ファイルが来ていればここは動かない")
        return ''

    src_file_name = os.path.basename(input_key)
    print(src_file_name)
    in_bucket.download_file(input_key, '/tmp/' + src_file_name)


    cmd = 'convert /tmp/' + src_file_name + ' /tmp/out' + out_ext

    print(cmd)
    proc = subprocess.run('rm /tmp/out' + out_ext, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    print('STDOUT: {}'.format(proc.stdout))
    print('STDERR: {}'.format(proc.stderr))

    src_prefix = input_bucket + '/'
    dst_prefix = output_bucket + '/'

    result_s3_path = (src_file_name).replace(src_prefix, dst_prefix, 1)
    ext = os.path.splitext(input_key)[1]
    result_s3_path = result_s3_path[::-1].replace(ext[::-1], out_ext[::-1], 1)[::-1]
    s3_uplodad(result_s3_path, '/tmp/out' + out_ext)

def s3_uplodad(key, local_path):
    if os.path.exists(local_path):
        print('Size: {}'.format(os.path.getsize(local_path)))
        data = open(local_path, 'rb')
        out_bucket = s3.Bucket(output_bucket)
        out_bucket .put_object(Key=key,Body=data)
        data.close()
    else :
        print("アップロードするファイルがない")

  ```

  ## S3にHEICをアップロードして、処理後のJPEGをダウンロードするプログラムを書く
  　引数にファイルパスを取り、そのファイルをS3へアップロード、処理後のファイルをポーリングで待つプログラムをPHPで書きました。タイムアウトの実装はまじめに実装するならばalarmを使うべきなのでしょうが、PHPを実行する条件によっては難しいのでsleepにしました。
   

   ```php
<?php
// lambda.php
include('./vendor/autoload.php');

use Aws\S3\S3Client;
use Aws\S3\Exception\S3Exception;

$srcBucket = 'to-jpg-src';
$dstBucket = 'to-jpg-dst';

$filenameBase = randStr(32);

$s3 = new S3Client([
    'version' => 'latest',
    'region' => 'ap-northeast-1',
    'credentials' => [
        'key' => '',
        'secret' => '',
    ],
]);

if ($argc != 2) {
    echo "Usage: php script.php <arg1>\n";
    return;
}

$srcPath = $argv[1];
if (!file_exists($srcPath)) {
    echo "file not exists\n";
    return;
}

$extension = pathinfo($srcPath)['extension'];

$key = $filenameBase . '.' . $extension;
putS3($srcBucket, $key, $srcPath);

$timeout = microtime(true) + 1800;//30分でタイムアウト
$key = $filenameBase . '.' . 'jpg';
pollS3($dstBucket, $key, $srcPath . '.jpg', $timeout);

function putS3($bucket, $key, $localFile){
    global $s3;
    try {
        $result = $s3->putObject([
            'Bucket' => $bucket,
            'Key' => $key,
            'Body' => fopen($localFile, 'r')
        ]);
    } catch (S3Exception $e) {
        echo "Error: {$e->getMessage()}\n";
    }
}

function pollS3($bucket, $key, $localFile, $timeout){
    global $s3;
    $pollingInterval = 1;
    while (true) {
        if (microtime(true) > $timeout){
            return 1;//timeout
        }
        try {
            $result = $s3->doesObjectExist($bucket, $key);
            if ($result) {
                $getObjectResult = $s3->getObject([
                    'Bucket' => $bucket,
                    'Key'    => $key,
                ]);
                $directory = dirname($localFile);
                if (!is_dir($directory)) {
                    mkdir($directory, 0755, true);
                }
                $outFile = fopen($localFile, 'w');
                while (!$getObjectResult['Body']->eof()) {
                    fwrite($outFile, $getObjectResult['Body']->read(1024));
                }
                fclose($outFile);
                return 0;
            }
            sleep($pollingInterval);
        } catch (AwsException $e) {
            echo "Error: {$e->getMessage()}\n";
            return 1;
        }
    }
}

function randStr($length){
    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $randomString = '';
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, strlen($characters) - 1)];
    }
    return $randomString;
}

   ```

## ↑で作成したプログラムをマルチプロセスで実行する
　特定ディレクトリ内で拡張子が.heicのファイルを列挙し、先ほどのプログラムをマルチプロセスで非同期に呼び出すコードを書きました。
 
```php
<?php

$directory = "./img";

$heicFiles = [];


listHeicFiles($directory);

$descriptorspec = [
    0 => ['pipe', 'r'], 
    1 => ['pipe', 'w'], 
    2 => ['pipe', 'w']   
];

foreach ($heicFiles as $file) {
    $command = "php ./lambda.php \"$file\"";
    echo "{$command}\n";
    proc_open($command, $descriptorspec, $pipes);
}

function listHeicFiles($dir) {
    $files = scandir($dir);
    foreach ($files as $file) {
        if ($file === '.' || $file === '..') {
            continue;
        }
        $filePath = $dir . '/' . $file;

        if (is_dir($filePath)) {
            listHeicFiles($filePath);
        } else {
            if (pathinfo($file, PATHINFO_EXTENSION) === 'heic') {
                global $heicFiles;
                $heicFiles[] = $filePath;
            }
        }
    }
}
```

この方法では処理のスループットは通信状況に依存してしまいますが、私の環境では128枚のHEIC画像をJPEGに変換するのに10秒ほどかかりました。