3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-02-27

概要

個人的に撮り貯めた数万枚の写真の整理をするにあたり、
容量節約のため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

③先ほどのスクリプトを起動します。下記ウィンドウが立ち上がるので
jpegファイルのフォルダとheicファイルの保存先フォルダを指定します。
同一フォルダでも大丈夫です。
OKボタンをクリックすると処理が開始されます。
10MBのjpegファイルの変換に10秒程度かかりました。
image.png

ファイル名先頭に'r_'が付加されたheicファイルが生成されました。
ファイル容量は10.0MB⇒5.85MB(48%減)になっています。
image.png

jpegファイルからExifタグ情報取得

続いてjpegファイルのExif情報取得。

image.png

jpegファイルからExifタグ情報取得コード(C#)

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ファイルから取得した情報を元に、
下記関数にファイルパスと共に投げると情報が更新されます。
また、同時にバックアップのファイルも生成されました。

UpdateExifInfoWithExifTool(heicFilePath, originalDateTime, make, model, apertureValue, exposureTime, isoSpeed.ToString(), exposureCompensation, focalLength, meteringMode, flashMode);
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

参考サイト

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?