はじめに
iOS(11以降)では写真を撮影したときに、特に設定をしていなければHEIFと呼ばれる方式で画像を保存します。拡張子はHEICです。この方式は高画質かつファイルサイズが小さいので便利そうなのですが、Apple系のOS以外ではまだサポートが充実していません。今回はHEIC画像を一括(バッチ)でJPEGに変換する方法をいくつか試してみます。
Windows版ImageMagickを使う
ImageMagickは様々なフォーマットに対応したツールで、最近HEICを読み込めるようになりました。Windowsバッチで次のように書くと、batファイルを設置した場所と同じ階層のHEICファイルを全てJEPGに変換できます。PATHを通すことができない場合はImageMagickの場所をフルパスで書くことで実行できます。
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
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" ]
# 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
// 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
$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秒ほどかかりました。