広告 プログラミング言語-Perl

11【高難度】JPEGファイルの更新日を撮影日等に修正する

JPEGファイルをバックアップすると、更新日がバックアップを取った日に変わってしまいます。スマートフォン等の画像Viewerは、撮影日ではなく、更新日でソートすることが多いので、画像VIewerの画像表示順番が狂って困ることがありますよね。

このPerlスクリプトは、JPEGの撮影日・作成日をExifより取得し、更新日を撮影日等の古い日付に変更します。
ついでに、ファイル名の先頭に西暦日付を付加するためのBATファイル案を作成します。(安全のためにdate_time_change_bat.txtとして出力)
txtファイルの内容を確認後、問題がなさそうなら「date_time_change.bat」と,ファイル拡張子を変更してから、実行してください。
BAT処理をすると、また更新日が本日の日付になってしまいます。もう一度このperlスクリプトを実行したら、更新日が撮影日に修正されます。

コメントに記載しているとおり、このスクリプトを実行するにはモジュール「CPAN,Win32:API,ExifTool」を事前にインストール&最新化する必要があります。繰り返しになりますが、スクリプトは必ずUTF-8に変換してから実行してください。

このスクリプトが何をしているのか、細かくコメントを付けたので、じっくり読んで下さい。
具体的な処理手順を見たい場合は、our $debug=1; としたら、ずらずら~っとメッセージが出てきます。
(実は、まだ改良の余地がある)うまく処理できないときは、改造してみてね。

なお、このスクリプトはWindows のCP932で実行することを前提に書かれています。2025年1月に、Windows11のStrawberry Perl最新版で実行確認済みです。
もし、このスクリプトをWindows以外のOSで使う場合は、&de_sjis()、&en_sjis()を、&de_utf8()、&en_utf8()等、適切なOS採用文字コードに修正してください。

jpeg_date_change.pl

#!/usr/bin/perl

# JPEGファイルの撮影日時・作成日を調査し、更新日を古い日付に変更する。
# ついでにファイル名の頭に日付を追加する為のBATファイルを作成するスクリプト

# 必ずソースをUTF-8で記載すること。

# 再帰呼び出しするので、サブフォルダも全て処理します。
# 1回目の呼び出しで更新日を、最も古い日付(作成日または撮影日)に変更します。
# ついでに「date_time_change_bat.txt」というファイルも出力します。
# もし、ファイル名の頭に日付を付けたい場合、このtxtファイルの拡張子をBATに変更して実行します。
# その後に、もう一度このスクリプト実行して更新日を変更します。

# (何故か更新日付しか変わらない。作成日は変更出来ない?今後要調査)

use strict;
use warnings;
use Encode;
use utf8;

use Win32;
use Cwd;
use Time::Local;
use Time::Piece;

use Image::ExifTool;

# 下記CPANモジュールが必要です。CPANを最新化し、Win32:API,ExifToolモジュールをインストール(最新化)する
# perl -MCPAN -e shell
#  install CPAN
#  install Win32:API
#  install Image:ExifTool
#  q


binmode STDIN, ":encoding(cp932)";   # 入力なのに、何故かencoding
binmode STDOUT, ":encoding(cp932)";
binmode STDERR, ":encoding(cp932)";

# ファイルハンドル<IN>等からの入力時はdecodeが必要
# ファイルハンドル<OUT>等への出力時はencodeが必要

our $debug=0;

#--------------------------------------------

#実行中のplスクリプトフルパスを取得
our $ScriptDir=&de_sjis($0);
$ScriptDir=~s/\\[^\\]+\.pl$//; # 最後の'\'も削除する。
&vv('$ScriptDir',$ScriptDir);

# BATファイルは、内容を確認するために、TXTファイルとして出力する。(使用する場合にBATに変更する)
open(OUT,'>date_time_change_bat.txt');

# スクリプトがあるフォルダから、再帰的呼び出しを実行
&showdir($ScriptDir,'');

close(OUT);

print '[Finish!]';
<>;
exit;

#--------------------------------------------
# sub

# 再帰的呼び出しを行うメインサブルーチン
sub showdir {
    my ($rootdir,$indir) = @_;

    print $rootdir.$indir,"\n";
    opendir(IN,&en_sjis($rootdir.$indir.'\\'));
    my @files_s=readdir(IN);    # Windowsでは@filesにCP932でファイル名とフォルダ名が格納される
    closedir(IN);

    for my $fname_s (sort @files_s){
        my $fname=&de_sjis($fname_s); # ファイル名・フォルダ名をcp932からPerl内部形式にデコードする

        if($fname ne '.' && $fname ne '..') {
            # 特殊フォルダ名 '.','..' でない場合
            
            # $path にファイル名・フォルダ名のパスを格納
            my $path = $indir . '\\' . $fname;
            &vv('$rootdir.$path',$rootdir.$path);

            if(-d &en_sjis($rootdir.$path)){ # Windowsでは、cp932にエンコードしないとフォルダか否かcheck出来ない
                # もしフォルダの場合、再帰呼び出し 再帰呼び出しをしたくなければ、下記の行をコメントアウトする。
                &showdir($rootdir,$path);
            } else {
                # フォルダ名ではなく、ファイル名
                if ($fname =~/\.(jpg|jpeg)$/i) {
                    # 拡張子がjpg又はjpegの場合のみ処理する(大文字・小文字を区別しないモード「i」
                    
                    # Exifデータを格納する連想配列を準備し初期化する
                    my %options=();

                    # ExifToolオブジェクトを作る
                    my $exifTool = new Image::ExifTool;

                    # 画像ファイルからメタ情報を連想配列%optionsに格納。
                    # ファイル名はcp932にエンコードして渡す必要がある
                    # データを格納してもらうために、リファレンスを渡す必要がある(\%options)
                    $exifTool->ExtractInfo(&en_sjis($rootdir.$indir.'\\'.$fname), \%options);

                    # 画像ファイル中の有効なExifタグを配列@tagListに取り込む
                    my @tagList = $exifTool->GetFoundTags('File');

                    # 列挙したExifタグの値に'Date' を含む行を抽出。ProfileDateTime以外で最も古い日付を抽出。
                    # 日付のセパレータがコロン
                    my $OldDate='9999:12:31 23:59:59';  #19文字 最も古い時刻を初期値とする Exifでは日付も':'でセパレートされている
                    for my $tag (@tagList) {
                        if ($tag=~/Date/i) {
                            # Date を含むタグの場合
                            
                            # データを取得する
                            my $Value=$exifTool->GetValue($tag);

                            $Value=~s/\s+$//; # 末尾の空白を消す
                            $Value=~s/T/ /; # Tを半角スペースへ置換。
                            $Value=~s/\-/\:/g; # 2023-01-01を2023:01:01へ置換。
                            if ($tag =~ /(ProfileDateTime|DateDisplayFormat|DateStampMode)/) {
                                # ProfileDateTime,DateDisplayFormat,DateStampMode は無視する(使えなかった)
                            } else {
                                if ($Value =~/^\d\d\d\d\:\d\d\:\d\d$/) {
                                    # データに時刻が無く日付のみの場合、時刻'00:00:00'を追加
                                    $Value .= ' 00:00:00';
                                } elsif($Value =~ /^\d\d\d\d\:\d\d\:\d\d \d\d\:\d\d\:\d\d$/) {
                                    # 日付時間の形ならOK
                                    
                                } elsif($Value =~ /^\d\d\d\d\:\d\d\:\d\d \d\d\:\d\d\:\d\dZ$/) {
                                    # 末尾にZが付いている場合(日付時間 GPS)最初の19文字だけ採用
                                    $Value=substr($Value,0,19);
                                } elsif($Value =~ /^\d\d\d\d\:\d\d\:\d\d \d\d\:\d\d\:\d\d(\+|\-|\:)\d\d\:\d\d$/ || $Value =~/^\d\d\d\d\:\d\d\:\d\d \d\d\:\d\d\:\d\d\.\d+(\+|\-|\:)\d\d\:\d\d$/) {
                                    # 日付時間GMT +09:00 GMTは無視する。最初の19文字だけ採用
                                    $Value=substr($Value,0,19);
                                } elsif($Value =~ /^\d\d\d\d\:\d\d\:\d\d \d\d\:\d\d\:\d\d\.\d+$/) {
                                    # 日付時間に2~6桁数字が付加されている場合も、最初の19文字だけ採用
                                    $Value=substr($Value,0,19);
                                } else {
                                    # 未知の書式の場合は、念のためTXTファイルにTag名とValueを出力しておく(今後の研究のため)
                                    print OUT &en_sjis($tag.":\t".$Value."\n");
                                }
                                my $NewDate=$Value;
                                if (('1900:01:01 00:00:00' lt $NewDate ) && ($OldDate gt $NewDate)) {
                                    # 最も古い日付を採用する 文字列で大小比較している
                                    $OldDate=$NewDate;
                                }
                            }
                        }
                    }
                    &vv('$OldDate',$OldDate);
                    
                    # ':'セパレートされている日付部分を、'/'セパレートに変換する。(念のため、4桁年、2桁月、2桁日の場合のみ処理している)
                    # 時刻部分は、':'セパレートのまま
                    $OldDate =~ s/^(\d\d\d\d)\:(\d\d)\:(\d\d)/$1\/$2\/$3/;
                    &vv('$OldDate 2',$OldDate);

                    #Perl 日付型に変換
                    my $t = Time::Piece->strptime($OldDate, '%Y/%m/%d %H:%M:%S');

                    my $year=$t->year;
                    my $mon=$t->mon;
                    my $mday=$t->mday;
                    my $hour=$t->hour;
                    my $minute=$t->minute;
                    my $sec=$t->sec;

                    print 'Last Date:'.$year.'/'.$mon.'/'.$mday.' '.$hour.':'.$minute.':'.$sec,"\n";

                    # エポック日付形式に変換
                    my $any_epoch = timelocal($sec, $minute, $hour, $mday, $mon - 1, $year - 1900); 

                    &vv('$any_epoch',$any_epoch);
                    print 'File:'.$rootdir.$indir.'\\'.$fname."\n";

                    # ファイル名の頭につける日付文字列を作成
                    my $date_header=sprintf('%4.4d%2.2d%2.2d',$year,$mon,$mday);
                    
                    # 既にファイル名の先頭が日付になっていたら、名前を変更しない
                    if ($fname =~ /^$date_header/) {    # /^${date_header}/ と記述してもOK
                    } else {
                        print OUT &en_sjis('ren "'.$rootdir.$indir.'\\'.$fname.'" "'.$date_header.'_'.$fname.'"'."\n");
                    }

                    # 更新日をエポックタイムに修正する
                    utime ($any_epoch,$any_epoch,$rootdir.$indir.'\\'.$fname);
                    
#<>; デバッグするときに、1ファイルずつ止めながら処理内容を確認するためのストッパー デバッグする時に、コメントを外す

                }
            }
         }
    }
}


#--------------------------------------------
# encode,debug tools

sub en_jis
{
    my ($buf)=@_;
    encode('iso-2022-jp',$buf);
}

sub de_jis
{
    my ($buf)=@_;
    decode('iso-2022-jp',$buf);
}

sub en_sjis
{
    my ($buf)=@_;
    encode('cp932',$buf);
}

sub de_sjis
{
    my ($buf)=@_;
    decode('cp932',$buf);
}

sub en_utf8
{
    my ($buf)=@_;
    encode('utf-8',$buf);
}

sub de_utf8
{
    my ($buf)=@_;
    decode('utf-8',$buf);
}
sub en_euc
{
    my ($buf)=@_;
    encode('euc-jp',$buf);
}

sub de_euc
{
    my ($buf)=@_;
    decode('euc-jp',$buf);
}


sub vv
{
    my($Name,$Value)=@_;
    if($debug) {
        print $Name.'=['.$Value.']',"\n";
    }
}

# 慣習的に1を返す(昔のPerlスクリプトの名残 なくても動く)
1;

ソースは必ずUTF-8に変換しよう(解説ページへ)

「cmd.exe」で「perl スクリプト名.pl」で実行(解説ページへ)

-プログラミング言語-Perl