HEICファイルをブラウザでJPEGに変換する最良の方法 - heic-toライブラリの紹介

## はじめに

ウェブアプリケーションでHEIC形式（iPhoneで撮影された写真に多く使われる形式）を扱う際、その変換処理は思いのほか難しい問題でした。本記事では、HEIC→JPEG変換のための複数のライブラリを比較し、最終的に「heic-to」ライブラリを使用して安定した変換処理を実現した経験を共有します。

## 背景：HEIC形式とは

HEIC（High Efficiency Image Container）は、Appleが採用している画像フォーマットで、JPEG形式と比較して同等の画質でありながら、ファイルサイズは約半分になる効率的なフォーマットです。しかし、このフォーマットはブラウザでの互換性に問題があり、多くのウェブアプリケーションではJPEGやPNGへの変換が必要になります。

## 困ったこと：他のライブラリでの問題点

### heic2anyの制限

最初に試したのは一般的な`heic2any`ライブラリでした。基本的なHEICファイルの変換には対応していましたが、以下の問題に直面しました：

1. 特定のiPhoneで撮影された写真で変換に失敗する
2. ファイルヘッダー判定が厳密すぎて、正規のHEICファイルを拒否することがある
3. エラーハンドリングが不十分で、変換失敗時のフォールバック対応が必要

```javascript
try {
  const jpegBlob = await heic2any({
    blob: heicBlob,
    toType: 'image/jpeg',
    quality: 0.9
  });
  // 成功処理
} catch (error) {
  // 多くの場合ここに落ちてしまう
  console.error('変換失敗:', error);
}
```

### libheif-jsの問題

次に試したのは低レベルライブラリである`libheif-js`でした。こちらもHEICデコードには対応していましたが：

1. 画像の描画処理で「Cannot read properties of undefined (reading 'set')」などのエラーが頻発
2. WebAssemblyの読み込みやメモリ管理に関する問題
3. 実装が複雑で、エラーハンドリングが難しい

```javascript
// libheif-jsでの複雑な実装例（一部）
const decoder = new libheif.HeifDecoder();
const data = await blob.arrayBuffer();
const images = decoder.decode(new Uint8Array(data));
if (!images || images.length === 0) {
  throw new Error('画像のデコードに失敗');
}
// さらに複雑な描画処理が続く...
```

## 解決策：heic-toライブラリ

様々なライブラリを試した結果、最終的に`heic-to`ライブラリが最も安定した変換結果を提供してくれました。

### heic-toの特徴

1. シンプルで使いやすいAPI
2. 内部で最適なライブラリを選択して変換を行う（ラッパーライブラリ）
3. iPhoneで撮影されたHEICファイルにも高い互換性
4. 適切なエラーハンドリング

### インストール方法

```bash
npm install heic-to
# または
yarn add heic-to
```

### 使用方法

非常にシンプルな使い方で、HEIC→JPEG変換が可能です：

```javascript
import { heicTo } from 'heic-to';

async function convertHeicToJpeg(heicBlob) {
  try {
    const jpegBlob = await heicTo({
      blob: heicBlob,
      type: 'image/jpeg',
      quality: 0.9  // 0.0～1.0の範囲で品質を指定
    });
    
    return jpegBlob;
  } catch (error) {
    console.error('HEIC変換エラー:', error);
    throw error;
  }
}
```

### PNG形式への変換も簡単

JPEGだけでなく、PNG形式への変換も同様に行えます：

```javascript
const pngBlob = await heicTo({
  blob: heicBlob,
  type: 'image/png'
});
```

## 実装例：フロントエンドでのHEIC→JPEG変換

以下は、実際のウェブアプリケーションでの実装例です：

```javascript
import { heicTo } from 'heic-to';

/**
 * HEIC形式をJPEGに変換
 * @param {Blob} blob - HEIC形式のBlob
 * @returns {Promise<Blob>} - JPEG形式のBlob
 */
async function convertHeicToJpeg(blob) {
  // HEICファイルかどうか検証
  const isValidHeic = await validateHeicFile(blob);
  if (!isValidHeic) {
    console.warn('有効なHEICファイルではありません');  
  }
  
  try {
    console.log('heic-toを使用して変換を試みます...');
    
    const jpegBlob = await heicTo({
      blob: blob,
      type: 'image/jpeg',
      quality: 0.9
    });
    
    if (jpegBlob && jpegBlob.type === 'image/jpeg') {
      console.log('変換に成功しました:', jpegBlob.size, 'bytes');
      return jpegBlob;
    }
    
    throw new Error('変換結果が無効です');
  } catch (error) {
    console.error('変換中にエラーが発生しました:', error);
    throw error;
  }
}
```

### HEICファイルの検証方法

参考までに、HEICファイルのヘッダーを検証する方法も紹介します：

```javascript
/**
 * HEICファイルかどうかを検証する
 * @param {Blob} blob - 検証するBlob
 * @returns {Promise<boolean>} - HEICファイルの場合はtrue
 */
async function validateHeicFile(blob) {
  try {
    // 先頭の12バイトを読み込む
    const headerBytes = await readBlobAsBuffer(blob.slice(0, 12));
    const header = Array.from(new Uint8Array(headerBytes))
      .map(byte => byte.toString(16).padStart(2, '0'))
      .join(' ');
    
    console.log('ファイルヘッダー: ', header);
    
    // ftypの位置を検索（通常は4バイト目以降）
    const headerText = String.fromCharCode.apply(null, new Uint8Array(headerBytes));
    const ftypIndex = headerText.indexOf('ftyp');
    
    if (ftypIndex >= 0) {
      // ftypの直後の4バイトがheicまたはheixかをチェック
      const typeBytes = new Uint8Array(headerBytes.slice(ftypIndex + 4, ftypIndex + 8));
      const typeText = String.fromCharCode.apply(null, typeBytes);
      
      if (typeText === 'heic' || typeText === 'heix') {
        console.log('HEIF/HEICファイルが正しく検出されました');
        return true;
      }
    }
    
    return false;
  } catch (error) {
    console.error('ファイル検証中にエラーが発生:', error);
    return false;
  }
}

// Blobの先頭部分をArrayBufferとして読み込む
function readBlobAsBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(new Error('ファイル読み込みエラー'));
    reader.readAsArrayBuffer(blob);
  });
}
```

## まとめ

HEIC形式の画像をウェブアプリケーションで処理する際は、`heic-to`ライブラリを使用することで、多くの問題を回避し、安定した変換処理を実現できます。特にiPhoneで撮影された写真を扱う場合に非常に役立ちます。

- 優れた互換性：様々なHEICファイルに対応
- シンプルなAPI：実装が容易
- 高い安定性：内部で最適なライブラリを選択

ブラウザ環境でのHEIC→JPEG変換にお困りの方は、ぜひ`heic-to`を試してみてください。

- heic-to: https://github.com/hoppergee/heic-to