🖼️

Node.jsでHEICを他のフォーマットに変換する方法

2024/11/10に公開

Node.jsでの画像編集

Node.jsにおいて画像処理を行うのにメジャーなライブラリはおそらくSharpになるかと思います。

https://github.com/lovell/sharp

このSharpですが、PNG,JPG、GIFなど多数のフォーマットをサポートしており非常に便利です。リサイズ、回転、色反転、フォーマット変換など非常に多くの機能を兼ね備えています。多くの画像関連の要件はこのモジュールで満たせると思います。

HEIC

HEICはiPhoneなどで使用されているフォーマットで圧縮効率がいいことが特徴のフォーマットになります。iPhoneで使用されているため、みなさんも意識せずに使用しているかと思います。

しかし、このHEICは以下の問題があります。

  • 比較的新しいフォーマットであることと特許の問題もあり、PCやスマホのソフトが対対応していない場合がある。*特許の問題についてはこちらのリンクを参照
  • 上記の問題でSharpもnpm installをただ行うだけではHEIC形式を扱うことができません。
  • Safari以外では基本的にそのまま表示させることもできません。CanIUseを参考に

なので、HEIC含めて画像編集を行いたい、または画像をユーザーにアップロードさせてそのまま表示させたいなどの要件の際にはHEICへの対処が必要です。

回避策

実はSafariから画像をアップロードする場合には、inputのacceptをaccept="image/jpg"などと指定すると自動で変換がかかります。iPhoneの場合、Chromeなどでも実態はSafariなので同じ挙動です。
なお、変換はブラウザで行われるようなので変換前の情報を取得することができません。

どうしてもHEICを受け付けないといけないとき

上記の回避策では不十分な場合、HEICを変換する方法は以下があります。

ブラウザで行いたい場合

heic2any
このライブラリはweb workerでHEICフォーマットを他のフォーマットに変換をしてくれるライブラリになります。
私の環境起因かもしれないですが5メガ程度のファイルは変換できましたが10メガ近いファイルだとブラウザでエラーが表示されました。(メモリ使用量がすごい。Issueをみるとメモリリークが発生するが対応されていない?)
https://github.com/alexcorvi/heic2any#readme

サーバーで行いたいとき

heic-convert
HEICの変換が可能。10メガ程度のファイルを変換したところ、メモリの使用量がrss1.5Gb程度となり、変換の時間もM2 Macで15秒ほどかかった。Lambdaでメモリを最大(CPUはメモリに合わせて変わる)にしても40秒以上かかった。
https://github.com/catdad-experiments/heic-convert#readme

サーバー側でより効率的に変換を行いたいとき

上記の結果でheic-convertではちょっと要件的に実用に耐えないため、一番最初に言及したSharpで試してみることにしました。
まずはローカルでHEICを変換できるようにするには以下のようにしてください。

  1. 変換に必要なソフトウェアをインストールします
    brew install vips libheif libde256 x265
    
  2. sharpのインストール
    インストール時に再度libvipsをビルドすることでMacに入っているlibheifなどを使用できます。
    *通常のnpm install sharpは事前にビルド済みのlibvipsなどをダウンロードして使用します。
    npm install --build-from-source node-addon-api node-gyp sharp
    

これでローカルのMacでHEICの変換ができるようになるはずです。
早速コードで試してみましょう。メモリ効率のいいstreamを使用してみます。

import fs from 'node:fs';
import { pipeline } from 'node:stream';
import { promisify } from 'node:util';
import sharp from 'sharp';

const HEICToJPG = async () => {
  const fileStream = fs.createReadStream('./test.heic');
  // defaultは80
  const transform = sharp().jpeg({ quality: 100 });
  const writeStream = fs.createWriteStream('./test.jpg');
  const asyncPipeline = promisify(pipeline);
  await asyncPipeline(fileStream, transform, writeStream);
};

環境によって違いはあるとは思いますが、私の環境ではM2 Macで10メガ程度のHEICで3~4秒ほどで、十分実用に耐えるスピード感に耐える環境でした。
サーバーで動かす際も事前にbrew installでinstallしたソフトをinstallすることで対応可能です。

Lambdaで動かしたい場合

Lambdaで動かしたい場合では、以下を使うと非常に簡単にHEICに対応したSharpを含んだLambda Layerを構築できます。
https://github.com/zoellner/sharp-heic-lambda-layer
使い方は以下です。

  1. cloneする
  2. sample-buildproject.yamlを実行すると、codebuildが作成される
  3. codebuildを実行するとLambda Layerが構築される
  4. 構築されたLambda LayerをLambdaで紐づける

Lambdaのアップロード用のファイルを作成するときに以下のようにesbuildを使うと便利です。

esbuild index.ts --bundle --outfile=index.js --platform=node --external:sharp

こうすることで、Lambdaにアップロードするファイルを生成でき、さらにバンドルからsharpを除外しているのでLambda Layerを見に行きます。

Discussion