画像ファイルの容量削減（jpeg⇒heic変換とExifタグ情報更新）

# 概要
個人的に撮り貯めた数万枚の写真の整理をするにあたり、
容量節約のためheic形式に変換して保存したいと考えました。
jpeg⇒heic形式への変換はGIMPのScript-Fu機能を使い実現
できたのですが、Exif情報は引き継がれませんでした。

写真を時系列で閲覧する際には撮影日時が無いと困るため、
jpegファイルのExif情報を取得しheicファイルのExif情報を
更新することにしました。更新には外部ツール’ExifTool’を
利用します。

C#のコードについては一部を除きChatGPTの生成を利用しています。
ChatGPT恐るべし！

また、私の環境では動きましたがPCの環境やExif情報によっては
不具合が出る可能性もあります。使用にあたってはバックアップを
確実に取ることをお勧めいたします。

# 環境
Windows11
VisualStudio C# .NET6.0
※NuGetで「ExifLib」をインストール。

# jpeg形式⇒heic形式への変換
①まずはGIMPをインストールします。
ダウンロード先：https://www.gimp.org/downloads/

②続いて下記スクリプトコードをテキストエディタ等へ転記して
ファイル名「script-fu-export_jpeg_to_heic.scm」として
下記フォルダへ保存します。
C:\Users\ユーザー名\AppData\Roaming\GIMP\2.10\scripts

画質（70）は適宜設定ください。

```
(define (export_jpeg_to_heic inDir inLoadType outDir)
  (let*
    (
      (varLoadStr "")
      (varFileList 0)
    )
    (set! varLoadStr
      (cond 
        (( equal? inLoadType 0 ) ".jpg" )
        (( equal? inLoadType 1 ) ".bmp" )
        (( equal? inLoadType 2 ) ".png" )
        (( equal? inLoadType 3 ) ".gif" )
      )
    )
    (set! varFileList (cadr (file-glob (string-append inDir "\\*" varLoadStr)  1)))

    (while (not (null? varFileList))
      (let* 
        ((filename (car varFileList))
          (short_filename (substring filename (+ (string-length inDir) 1) (string-length filename)))
          (extNum 4)
          (fileLenghNum (string-length  short_filename))
          (delExt (- fileLenghNum extNum))
          (short_filename (substring short_filename 0 delExt))
          (short_filename (string-append short_filename ".heic"))
          
          
          (image (car (gimp-file-load RUN-NONINTERACTIVE filename filename)))
          (drawable (car (gimp-image-get-active-layer image)))
          ;保存ファイル名の先頭に"r_"を追加
          (newfilename (string-append outDir "\\" (string-append "r_" short_filename)))
          
          
        )
        
        ;画質70で保存
        (file-heif-save RUN-NONINTERACTIVE image drawable newfilename short_filename 70 0)
        
        (gimp-image-delete image)
        
      )
      (set! varFileList (cdr varFileList))
    )

    (gimp-patterns-refresh)
  )
)

(script-fu-register "export_jpeg_to_heic"
  "<Image>/Script-Fu/Export Jpeg to Heic..."
  "Export Jpeg to Heic."
  "appin"
  "appin"
  "November 2018"
  ""
  SF-DIRNAME    "Load from" ""
  SF-OPTION     "Load File Type" (list "jpg" "bmp" "png" "gif")
  SF-DIRNAME    "Save to"  ""
)
```

GIMPを起動するとメニューが追加されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611426/c9e094ac-f4a4-8a13-369b-c7493d631ed2.png)

③先ほどのスクリプトを起動します。下記ウィンドウが立ち上がるので
jpegファイルのフォルダとheicファイルの保存先フォルダを指定します。
同一フォルダでも大丈夫です。
OKボタンをクリックすると処理が開始されます。
10MBのjpegファイルの変換に10秒程度かかりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611426/5568f047-074e-3a7f-2eec-40d436fdee0e.png)

ファイル名先頭に'r_'が付加されたheicファイルが生成されました。
ファイル容量は10.0MB⇒5.85MB（48%減）になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611426/e6ba0514-db8f-beca-52ff-f3ff794e41fa.png)


# jpegファイルからExifタグ情報取得
続いてjpegファイルのExif情報取得。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611426/bae2869a-a6cc-d759-ebad-125ecfd303cf.png)



# jpegファイルからExifタグ情報取得コード（C#）
``` cs
using System;
using System.IO;
using ExifLib;
using System.Drawing.Imaging;
using System.Text;

namespace Exif情報取得
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            string jpegFilePath = "C:\\Users\\ukiuk\\Desktop\\moto\\IMG_5348.JPG";

            try
            {
                ExifReader reader = new ExifReader(jpegFilePath);

                // 撮影日時
                DateTime? takenDate = GetTakenDate(jpegFilePath);

                // 製造元
                string make = GetTagValue<string>(reader, ExifTags.Make);

                // カメラモデル
                string model = GetTagValue<string>(reader, ExifTags.Model);

                // 絞り値
                string apertureValue = GetApertureValue(reader);

                // 露出時間
                string exposureTime = GetExposureTime(reader);

                // ISO速度
                ushort? isoSpeed = GetTagValue<ushort?>(reader, ExifTags.ISOSpeedRatings);

                // 露出補正
                string exposureCompensation = GetExposureCompensation(reader);

                // 焦点距離
                string focalLength = GetFocalLength(reader);

                // 測光モード
                string meteringMode = GetMeteringMode(reader);

                // フラッシュモード
                string flashMode = GetFlashMode(reader);

                // 取得した情報を表示
                listBox1.Items.Add($"撮影日時: {takenDate}");
                listBox1.Items.Add($"製造元: {make}");
                listBox1.Items.Add($"カメラモデル: {model}");
                listBox1.Items.Add($"絞り値: {apertureValue}");
                listBox1.Items.Add($"露出時間: {exposureTime}");
                listBox1.Items.Add($"ISO速度: {isoSpeed}");
                listBox1.Items.Add($"露出補正: {exposureCompensation}");
                listBox1.Items.Add($"焦点距離: {focalLength}");
                listBox1.Items.Add($"測光モード: {meteringMode}");
                listBox1.Items.Add($"フラッシュモード: {flashMode}");
                reader.Dispose();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }

        static T GetTagValue<T>(ExifReader reader, ExifTags tag)
        {
            object val;
            if (reader.GetTagValue(tag, out val))
            {
                return val != null ? (T)val : default(T);
            }
            else
            {
                return default(T);
            }
        }

        //撮影日時
        static DateTime? GetTakenDate(string imgFile)
        {

            using (Bitmap bmp = new Bitmap(imgFile))
            {
                foreach (PropertyItem item in bmp.PropertyItems)
                {
                    if (item.Id == 0x9003 && item.Type == 2) // Exif情報から撮影日時を取得
                    {
                        string val = Encoding.ASCII.GetString(item.Value).Trim('\0');
                        DateTime dt = DateTime.ParseExact(val, "yyyy:MM:dd HH:mm:ss", null);
                        //Console.WriteLine($"撮影日時: {dt}");
                        return dt;
                    }
                }
                return null;
            }
        }

        //絞り値取得
        static string GetApertureValue(ExifReader reader)
        {
            double? val;
            if (reader.GetTagValue(ExifTags.FNumber, out val))
            {
                return val.HasValue ? $"f/{val.Value:F1}" : "N/A";
            }
            else
            {
                return "N/A";
            }
        }

        //露出時間取得
        static string GetExposureTime(ExifReader reader)
        {
            double? val;
            if (reader.GetTagValue(ExifTags.ExposureTime, out val))
            {
                if (val.HasValue)
                {
                    // 1/200 のような形式に変換する
                    return $"1/{(int)(1 / val.Value)}";
                }
                else
                {
                    return "N/A";
                }
            }
            else
            {
                return "N/A";
            }
        }

        //露出補正取得
        static string GetExposureCompensation(ExifReader reader)
        {
            double? val;
            if (reader.GetTagValue(ExifTags.ExposureBiasValue, out val))
            {
                return val.HasValue ? val.Value.ToString() : "N/A";
            }
            else
            {
                return "N/A";
            }
        }

        //焦点距離
        static string GetFocalLength(ExifReader reader)
        {
            double? val;
            if (reader.GetTagValue(ExifTags.FocalLength, out val))
            {
                return val.HasValue ? val.Value.ToString() : "N/A";
            }
            else
            {
                return "N/A";
            }
        }

        //測光モード
        static string GetMeteringMode(ExifReader reader)
        {
            ushort? val;
            if (reader.GetTagValue(ExifTags.MeteringMode, out val))
            {
                if (val.HasValue)
                {
                    switch (val.Value)
                    {
                        case 0: return "Unknown";
                        case 1: return "Average";
                        case 2: return "CenterWeightedAverage";
                        case 3: return "Spot";
                        case 4: return "MultiSpot";
                        case 5: return "Pattern";
                        case 6: return "Partial";
                        case 7: return "Other";
                        case 255: return "Not defined";
                        default: return val.Value.ToString(); // 未定義の値の場合、そのまま数値を返す
                    }
                }
                else
                {
                    return "N/A";
                }
            }
            else
            {
                return "N/A";
            }
        }

        //フラッシュモード
        static string GetFlashMode(ExifReader reader)
        {
            ushort? val;
            if (reader.GetTagValue(ExifTags.Flash, out val))
            {
                if (val.HasValue)
                {
                    switch (val.Value)
                    {
                        case 0: return "No flash";
                        case 1: return "Fired";
                        case 5: return "Fired, return not detected";
                        case 7: return "Fired, return detected";
                        case 9: return "On, did not fire";
                        case 13: return "On, return not detected";
                        case 15: return "On, return detected";
                        case 16: return "Off, did not fire";
                        case 24: return "Auto, did not fire";
                        case 25: return "Auto, fired";
                        case 29: return "Auto, fired, return not detected";
                        case 31: return "Auto, fired, return detected";
                        case 32: return "No flash function";
                        case 65: return "Off, no flash function";
                        case 69: return "Fired, red-eye reduction";
                        case 73: return "Fired, red-eye reduction, return not detected";
                        case 75: return "Fired, red-eye reduction, return detected";
                        case 77: return "On, red-eye reduction";
                        case 81: return "On, red-eye reduction, return not detected";
                        case 83: return "On, red-eye reduction, return detected";
                        case 93: return "Auto, did not fire, red-eye reduction";
                        case 95: return "Auto, fired, red-eye reduction";
                        case 99: return "Auto, fired, red-eye reduction, return not detected";
                        case 103: return "Auto, fired, red-eye reduction, return detected";
                        default: return val.Value.ToString(); // 未定義の値の場合、そのまま数値を返す
                    }
                }
                else
                {
                    return "N/A";
                }
            }
            else
            {
                return "N/A";
            }
        }
    }
}
```
# heicファイルのExifタグ情報更新
jpegファイルから取得した情報を元にheicファイルのExif情報を更新します。
外部ツールとして’ExifTool’をダウンロード、適当なフォルダに解凍します。
ダウンロード先：https://exiftool.org/
Windows Executableをダウンロードします。

# heicファイルのExifタグ情報変更
先ほどjpegファイルから取得した情報を元に、
下記関数にファイルパスと共に投げると情報が更新されます。
また、同時にバックアップのファイルも生成されました。


``` cs
UpdateExifInfoWithExifTool(heicFilePath, originalDateTime, make, model, apertureValue, exposureTime, isoSpeed.ToString(), exposureCompensation, focalLength, meteringMode, flashMode);
```

``` cs
static void UpdateExifInfoWithExifTool(string filePath, DateTime newTakenDate, string newMake, string newModel, string newApertureValue, string newExposureTime, string newISO, string newExposureCompensation, string newFocalLength, string newMeteringMode, string newFlashMode)
        {
            // ExifToolのパスを指定します。適切なパスに変更してください。
            string exifToolPath = @"C:\Users\ユーザー名\Desktop\exiftool-12.77\exiftool.exe";

            // ExifToolを呼び出して、EXIF情報を書き換えます。
            ProcessStartInfo startInfo = new ProcessStartInfo
            {
                FileName = exifToolPath,
                Arguments = $"-DateTimeOriginal=\"{newTakenDate:yyyy:MM:dd HH:mm:ss}\" " +
                            $"-Make=\"{newMake}\" " +
                            $"-Model=\"{newModel}\" " +
                            $"-FNumber=\"{newApertureValue}\" " +
                            $"-ExposureTime=\"{newExposureTime}\" " +
                            $"-ISO=\"{newISO}\" " +
                            $"-ExposureCompensation=\"{newExposureCompensation}\" " +
                            $"-FocalLength=\"{newFocalLength}\" " +
                            $"-MeteringMode=\"{newMeteringMode}\" " +
                            $"-Flash=\"{newFlashMode}\" " +
                            $"\"{filePath}\"",
                RedirectStandardOutput = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            using (Process process = Process.Start(startInfo))
            {
                process.WaitForExit();
                if (process.ExitCode == 0)
                {
                    Console.WriteLine("EXIF information updated successfully.");
                }
                else
                {
                    Console.WriteLine("Failed to update EXIF information.");
                }
            }
        }
```

無事にheicファイルのExif情報が更新されました。
（同時に更新日時も修正していますがここでは割愛します。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611426/688ca493-65ec-b552-d87d-6a8c13343599.png)




# 参考サイト
https://saitottammas.wordpress.com/2018/11/10/gimp-script-fu%E3%81%A7%E7%94%BB%E5%83%8F%E4%B8%80%E6%8B%AC%E5%A4%89%E6%8F%9B/

https://qiita.com/appin/items/173abe8fe169789d0617

https://gimp.jp.net/writinggimpscripts.html

https://www.vieas.com/exif23.html