Xiaomi デバイスが出力する HEIF 画像ファイルの Exif が巨大すぎる件

Xiaomi 14 Ultra の出力した HEIF ファイルを ImageMagick で JPEG に変換して Exif が壊れたケースのメモ書きです。

| Xiaomi 14 Ultra |
| :-: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/97135169-270e-c8dc-f630-6cc10333e2a9.png" width="50%" /> |
| (c) https://www.mi.com/jp/product/xiaomi-14-ultra/ |

ふと、気になるツイートが目に入り、データを譲って頂いて解析できました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">14Ultraが生成する画像のメタデータが怪しくてImageMagickとExifToolで変換できねえ <img src="https://pbs.twimg.com/media/GPFJ9MabgAAQvl4?format=png&name=small" /> </p>&mdash; こて
(@k0_te) <a href="https://x.com/k0_te/status/1797299174356193393">2024年6月3日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- 原因は、HEIF 画像ファイルに含まれる Exif のデータ量が多すぎて、JPEG 画像ファイルの Exif に保存しきれない仕様上の制限です。
- 途中で途切れた Exif をつけます。(多分不具合)
- **Exif の MakerNote にサムネール画像が含まれるのを確認**しました、取り扱い注意です。

# 症状

```shell:
% convert IMG_XIAOMI_14ULTRA.HEIF IMG_XIAOMI_14ULTRA.JPG
convert: exif profile size exceeds limit and will be truncated `IMG_XIAOMI_14ULTRA.JPG' @ warning/jpeg.c/WriteProfiles/2207.
```

Exif Profile のサイズ超過で truncate、つまり最後の方を削除する警告です。

```shell:
% exiftool IMG_XIAOMI_14ULTRA.HEIF
(略)
Warning : [minor] Suspicious MakerNotes offset for tag 0x4d4d
```

こちらは、Exif 内の MakerNotes の解釈失敗です。あとで説明します。

# 解析

## Exif のデータサイズ

libheif の heif-info コマンドで簡単な解析が出来ます。

```shell:
% heif-info IMG_XIAOMI_14ULTRA.HEIF
(略)
metadata:
  Exif: 91352 bytes
```

Exif が 91352 bytes です。
JPEG は APP1 チャンクに Exif を載せるので、APP1 チャンクの最大サイズに制限されます。

![スクリーンショット 2024-06-06 1.27.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/b1c95313-a745-f47e-0706-ca999bbb7de8.png)


JPEG のチャンク長(Length)は 2bytes で表現し、その 2bytes を長さに含める為、(2bytes の最大値)0xffff - 2 で 0xfffd (= 65533 bytes) が Exif を詰められる最大サイズです。

ImageMagick は Exif を 65533 bytes だけ JPEG に格納するので、65534 byte目以降のデータ(25819 bytes)が消えます。

![スライド3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/e37c3fce-46da-720d-1dc7-664f80d8c4d9.png)

## multi-segment APP1

複数の APP1 に分けて保存するのを思いつきます。
いわゆる multiple chunk 対応です。先に結論を言うと悪手と思われます。

例えば ExifTool で JPEG の Exif データを膨らませ、複数 APP1 ができる例がこちらです。

- https://issuetracker.google.com/issues/263747161#comment7

```
JPEG APP1 (65533 bytes):
(略)
  Warning = [minor] File contains multi-segment EXIF
JPEG APP1 (71 bytes):
```

ところが、CIPA の Exif 仕様書を読む限り、明記はされていませんが、APP1 は一つのみと解釈した方が良さそうです。
というのも、APP1 と APP2 をよく並べて説明するのですが、APP2 は毎度しつこく複数を使う事があると説明しているのに、APP1 にその言及がない為です。

- https://www.cipa.jp/std/documents/j/DC-008-2012_J.pdf

|  |  |
|--|--|
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/a0955dc1-3020-6bd7-0eb2-b09ffe1c465b.png) |  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/b787a6a2-45a6-3b91-c867-a3c62c9232b0.png) <br /> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/c89879e6-4a39-150e-0171-e889dd672664.png) |


ImageMagick は巨大な Exif データを伴って JPEG を作るとき、65533 までは埋め込み、それ以降のデータを無視します。

- https://github.com/ImageMagick/ImageMagick/blob/main/coders/jpeg.c#L2200

```c:
if (LocaleCompare(name,"EXIF") == 0)
  {
    length=GetStringInfoLength(profile);
    if (length > 65533L)
      {
        (void) ThrowMagickException(exception,GetMagickModule(),     
                 CoderWarning,"ExifProfileSizeExceedsLimit","`%s'",
                 image->filename);
        length=65533L;
      }
    jpeg_write_marker(jpeg_info,XML_MARKER,GetStringInfoDatum(profile),
      (unsigned int) length);
}
```

生成はせずとも、複数 APP1 の読み込みに関して、PIL(Pillow) のように対応する事もありえます。壊れてるけど柔軟に対応するよ！とのコメント。

- https://github.com/python-pillow/Pillow/issues/7495
> So this image is actually incorrect. However, I've created PR #7496 to make Pillow more flexible and combine the EXIF markers.

## Exif が大きい理由

MakerNotes が大きいようです。

```shell:
% exiftool IMG_XIAOMI_14ULTRA.HEIF -MakerNotes -b > MakerNotes.bin
Warning: [minor] Suspicious MakerNotes offset for tag 0x4d4d - IMG_XIAOMI_14ULTRA.HEIF
-rw-r--r--  1 yoya  staff  65564  6  6 19:59 MakerNotes.bin
% exiftool MakerNotes.bin
ExifTool Version Number         : 12.76
File Name                       : mn.bin
Directory                       : .
File Size                       : 66 kB
File Modification Date/Time     : 2024:06:(略):48+09:00
File Access Date/Time           : 2024:06:(略):31+09:00
File Inode Change Date/Time     : 2024:06:(略):48+09:00
File Permissions                : -rw-r--r--
Warning                         : Processing JPEG-like data after unknown 268-byte header
Image Width                     : 320
Image Height                    : 240
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
Image Size                      : 320x240
Megapixels                      : 0.077
```

MakerNotes のバイナリを試しに頭から 1byteずつ scan して JPEG として解釈できる領域を切り出してみたところ、320x240 と 1440x1080 のサムネール画像が含まれていました。

![スライド4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/8a708cea-27e4-2297-464b-dc6d61f9c7d4.png)

## Exif 識別子

たまたま HEIF ダンプツールがあるので、それで確認。

```shell:
% composer require yoya/io_heif
% php  vendor/yoya/io_heif/sample/heiftree.php -f IMG_XIAOMI_14ULTRA.HEIF
(略)
[10049]: cdsc:10048 type:Exif method:0 ref:0 offset:16753 length:91352
    (mdat:0x4171,0x164d8) 00 00 00 0A FF E1 1A B6 45 78 69 66 00 00 49 49 ...
```

Exif の識別子は "Exif¥0¥0" ですが、Xiaomi の Exif データは微妙に狂ってます。
```
(通常)    "00 00 00 06 45 78 69 66 00 00"
(Xiaomi) "00 00 00 0A FF E1 1A B6 45 78 69 66 00 00"
```

(FFE1 は JPEG の APP1 マーカーそのものなので、単に不具合でゴミが入っているのではと。。)

お行儀悪いですが仕方ないので、ImageMagick は 2022年に対応しました。
- https://github.com/ImageMagick/ImageMagick/issues/5604
> Hi I see this garbage bytes on several XiaoMi devices. Here is one example.

## MakerNotes

各メーカーが自由に使える私的利用領域です。

- https://exiftool.org/makernote_types.html
- https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/MakerNotes.pm

この表とプログラム実装を見ると、Xiaomi Ultra 14 の Notes 構造にまだ対応してないようです。

```shell:
% convert IMG_XIAOMI_14ULTRA.HEIF IMG_XIAOMI_14ULTRA.exif
% php vendor/yoya/io_exif/sample/exifdump.php -f IMG_XIAOMI_14ULTRA.exif
(略)
        0x927C: Type:UNDEFINED Count:88 Offset:720 Data:Xiaomi^@^AMM^@^F^@(略)^@
        0x9290: Type:ASCII Count:4 Data:758^@
```

0x927C が MakerNotes のタグです。
Xiaomi^@^A を識別子として、その後を Exif (TIFFコンテナ)として読めば良さそうです。

# 解決策

この MakerNotes は Xiaomi デバイスしか認識できないので、他で表示する目的であれば、HEIF から MakerNotes を削ってから JPEG に変換すると良いでしょう。

![スライド5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/764eea79-5b5b-019a-e70a-bc9ab54e9334.png)

```shell:
% ls -l IMG_XIAOMI_14ULTRA.HEIF
-rw-rw-r--@ 1 yoya  staff  304862  6  4 02:00 IMG_XIAOMI_14ULTRA.HEIF
% exiftool -v -MakerNotes=  IMG_XIAOMI_14ULTRA.HEIF
======== IMG_XIAOMI_14ULTRA.HEIF
Rewriting IMG_XIAOMI_14ULTRA.HEIF...
  FileType = HEIC
  FileTypeExtension = HEIC
  MIMEType = video/quicktime
  Editing tags in: EXIF ExifIFD IFD0 ItemInformation MOV MakerNotes Meta
  Rewriting Meta
(略)
  Rewriting IFD1
    1 image files updated
% ls -l IMG_XIAOMI_14ULTRA.HEIF
-rw-rw-r--  1 yoya  staff  220256  6  7 00:21 IMG_XIAOMI_14ULTRA.HEIF
% convert  IMG_XIAOMI_14ULTRA.HEIF  IMG_XIAOMI_14ULTRA.JPEG
% 
```

MakerNotes を捨てられない場合、JPEG 側に 65533 制限がある以上、Exif データを全て載せるのは無理です。以下のような選択肢が考えられます、

- convert IMG〜.HEIF IMG〜.EXIF でデータを待避して別管理
- multi-segment APP1 対応ツールを使う。(exiftool ？)、ただし仕様から外れる可能性あり
- Exif の後ろが削れるのを受け入れる。(自分が必要とするデータが後ろにないのを確認した上で)

# 懸念

HEIF や JPEG のサムネール領域や Exif のサムネール領域でもなく、Exif の MakerNotes にサムネール画像を入れると、例えば編集ソフトで黒塗りにしたり、別画像に差し替えたりしても更新できずに残り続ける可能性ありそうですけど、大丈夫なのでしょうか？

昔、Photoshop3 位の頃に、JPEG 画像のサムネールで問題になったのを思い出します。