PythonでiPhone撮影の.heic写真を撮影日付でリネームしJPEGで保存

## 背景
私は、夫婦のそれぞれのiPhoneで撮影した写真をネットプリントでプリントアウトしアルバム整理をしています。
ただし私が使っているネットプリント・サービスはファイル名ごとにソートされ、夫婦で共有した写真を混ぜるとファイル名がバラバラなので、日付がバラバラで並んできて不便でした。また、写真の裏にファイル名のみが記載されるので、日付をわかるようにした上で、日付ごとに並んでプリントしたいと考えていました。

また、iPhoneで写真撮影をすると、デフォルトではHEICという形式で保存されます。HEICの詳細な説明は省きますが、このファイルをWindowsの標準で表示しようとすると厄介で扱いにくいためJPEGで保存してWindows上で表示したいと思っていました。（ただし画質は多少犠牲にする前提）

## やりたいこと
* iPhoneで撮影した写真のファイル名を撮影日付にしたい
* iPhone撮影にて標準で保存されるHEICファイルを、JPEGファイルとして保存し直したい

## やること
* HEICファイルを読み込み ([pillow-heif](https://pypi.org/project/pillow-heif/)を使う)
* 撮影日時を抜き出す (写真に埋め込まれたExifの情報を抽出)
* 撮影日時をファイル名にする (Exifの撮影日時をdatetimeにして、自分の好きな形式にする)
* JPEGに変換して保存する (PillowでJPEG保存)

## 前提
* Windows (他のOSでも動くと思うけど確かめてません）
* iPhone撮影の写真がWindows上の特定フォルダに保存されている
* Pythonを使う (Jupyter Notebookでやりました)

## 手順
### ライブラリインストール
pipでpillowとpillow-heifをインストールします。
```bash
$ pip install pillow pillow-heif
```

[pillow-heif](https://pypi.org/project/pillow-heif/)は、pillowでHEIC形式の画像を扱うことができるライブラリです。HEICのまま様々な処理ができますが、pillow-heifを利用して画像ファイルを読み込むだけで、意識することなく通常の`PIL.Image`オブジェクトのように扱うことができるようになります。


### Pythonコード
少し行数（ステップ）を長く書いていますが、ステップごとのわかりやすさ/読みやすさ重視の記述をしています。
詳しくコメントをつけていますが、ExifのDictについては後述します。

```python: HEICファイルを読み込み、撮影日付でリネームしJPEGで保存
from datetime import datetime
from pathlib import Path

from PIL import Image
from pillow_heif import HeifImagePlugin

# HEIC形式の写真が保存されているフォルダを指定
photo_dir = Path('C:\\my_photo')
# リネーム後の出力先フォルダを作成しておく
out_dir = photo_dir / Path('.\\rename')
out_dir.mkdir(exist_ok=True, parents=True)

# フォルダに保存されている.heicまたは.jpgファイルをリスト化（jpegで保存された写真も扱っていたため）
files = [*photo_dir.glob('*.heic'), *photo_dir.glob('*.jpg')]

for i, file in enumerate(files, 1):
    # HEICファイルをImageオブジェクトとして読み込み
    im = Image.open(file)
    # 埋め込まれたExif情報を読み込む(Exifオブジェクトを取得※後述)
    p_ex = im.getexif()
    p_len = len(files)
    try:
        # Exifから撮影日時を取得（ExifオブジェクトからDateTimeを取得※後述）
        p_date_str = p_ex[ExifTags.Base.DateTime]
        # Exifの日付形式から、datetimeオブジェクトに変換
        tm_dt = datetime.strptime(p_date_str, "%Y:%m:%d %H:%M:%S")
        # 日付-時刻という形式の文字列にする
        tm_str = tm_dt.strftime('%Y%m%d-%H%M%S')
        # 日付-時刻.jpgの拡張子をつけたファイル名とする
        f_name = tm_str + '.jpg'
        print(f'{i} / {p_len}: {file.name} -> {f_name}')
        # 出力フォルダにファイル名を加え、出力先のフルパスを作成する
        out_path = out_dir / f_name
        # JPEGで保存する。Exif情報も埋め込んでおく
        im.save(out_path, quality=80, exif=p_ex)
    except Exception as e:
        # Exifが埋め込まれてないファイルなどがあれば、エラーとして出力して処理は継続する
        print(f'Error: {file.name}, {e}')
```

### Exifから撮影日時の取得について
PillowとExifについて知らない中でリファレンスを見て初めて知ったのでメモとして残しておきます。

Pillowの`Image`オブジェクトの中には、`Exif`オブジェクトが存在します。
* [class PIL.Image.Exif](https://pillow.readthedocs.io/en/stable/reference/Image.html#classes)

この`Exif`オブジェクトから、Enumである`ExifTags`を使って画像に埋め込まれたExifの各項目を抽出可能になっています。
* [ExifTags Module](https://pillow.readthedocs.io/en/stable/reference/ExifTags.html#exiftags-module)

今回は撮影日時なので、[PIL.ExifTags.Base](https://pillow.readthedocs.io/en/stable/reference/ExifTags.html#PIL.ExifTags.Base)のsourceに記載されている`DateTime`を指定します。

```python: Exifの撮影日付を抽出する部分
p_ex = im.getexif()
p_date_str = p_ex[ExifTags.Base.DateTime]
print(p_date_str)
# 2024:09:16 13:07:05
```
今回やりたいこととは異なりますが、位置情報は[PIL.ExifTags.GPS](https://pillow.readthedocs.io/en/stable/reference/ExifTags.html#PIL.ExifTags.GPS)で取得できるようになっています。

Exifは規格化されているものの、すべてのタグの情報がすべてのカメラで保存されるわけではないので、iPhoneから取得できる情報は限られていますが、無事撮影日付を取得することができました。