はじまり
なんか画像の向きが忌々しい向きに戻っちゃったんだけど!?
せっかく縦向きにしたのに横向きに戻ってしまったなあ。
Webpに変換した画像の向きが戻ってしまった。
今回扱うケースは、拡張子を別のものへと変換した時に発生したものです。
「libwebp」などのライブラリで、JPGやPNGの画像ファイルをWEBP拡張子に変換しました。
すると、この元々のJPG画像が・・・
すると、WEBP画像へと変換したことによって横向きから縦向きになってしまいました!?
元々このJPG画像は、Androidでゲームを遊んでいる時の画面をスクショしたものです。そして、その画像は最初は横向きで保存されていたので、Androidの画像ビュワーアプリ上で縦向きに変換した、という経緯がありました。
戻す方法
1枚だけ回転させる。
この事象を解決するために、以下のようにしてPowerShellで画像を回転させます。とりあえず1枚回転させたい時はこの処理で足ります。
$inputPath = (Resolve-Path ".\20220729234911.jpg").Path;
$outputPath = Join-Path -Path (Get-Location) -ChildPath ".\20220729234911_orien.jpg";
$outputPath = $outputPath.Replace(".\", "");
Add-Type -AssemblyName System.Drawing;
$image = [System.Drawing.Image]::FromFile($inputPath);
$image.RotateFlip([System.Drawing.RotateFlipType]::Rotate90FlipNone);
$image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Jpeg);
$image.Dispose();
相対パスで処理させようとすると以下のようなエラーになるので、絶対パスに直す処理も含めています。
こちらは、[System.Drawing.Image]::FromFile()
を相対パスを使って実行した時のエラー。ちなみに、「.ctor」はコンストラクターのことらしいです。
New-Object : “1” 個の引数を指定して “.ctor” を呼び出し中に例外が発生しました: “使用されたパラメーターが有効ではありません。”
…
こちらは、$image.Save()
を相対パスを使って実行した時のエラー。
“2” 個の引数を指定して “Save” を呼び出し中に例外が発生しました: “値を Null にすることはできません。
パラメーター名:format”…
先程の$image.Save()
で[System.Drawing.Imaging.ImageFormat]::Webp
を引数に入れると変換できなかったので、別の方法でWebpにします。回転させた時はJpegをWebpには出来ないのかもしれません。Webpはビットマップ形式ではないですしね。
なので、回転させた画像を「Cwebp」というライブラリでWebpにします。詳しい方法は下記の記事で紹介しています。
そして、Webpになった画像は回転を保持した状態で、Windows上でもWeb上でも閲覧できるようになりました。
一括で複数枚回転させる。
それではこの処理を一括で複数枚の画像ファイルに適用できるようにします。
function Set-ImageOrientations([string]$orgSuffix) {
$updatedCount = 0;
$targetExt = ".jpg";
Write-Host ("{0}: aaaaaaaaaaaaaaaaaaaaaaaaaaaaa`n" -f $MyInvocation.MyCommand.Name);
Get-ChildItem *$targetExt | ForEach-Object {
$img = $_.FullName;
$isPathCorrect = Test-Path $img;
Write-Output $img;
Write-Output $isPathCorrect;
Write-Host ("{0}: cccccccccccccccccccccccccccc`n" -f $MyInvocation.MyCommand.Name);
if ($isPathCorrect -eq $false) {
continue;
}
$suffix = "_orien";
$suffixWithExt = "{0}{1}" -f $suffix, $targetExt;
$imgOriented = $img.Replace($targetExt, $suffixWithExt);
Set-ImageOrientation $img $imgOriented $orgSuffix;
$updatedCount++;
};
Write-Output("{0} images confirmed." -f $updatedCount);
Write-Host ("{0}: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`n" -f $MyInvocation.MyCommand.Name);
}
function Set-ImageOrientation([string]$srcRelPath, [string]$dstRelPath, [string]$suffix) {
$inputPath = (Resolve-Path $srcRelPath).Path;
$outputPath = $dstRelPath;
$outputPath = $outputPath.Replace(".\", "");
$relDirOwningLog = ".";
$strCmd = "{0}/exiftool.exe" -f $relDirOwningLog;
$argumentArray = @("-T", "-Orientation", $inputPath)
$arguments = $argumentArray -join " ";
$oResult = Invoke-ExternalCommand $strCmd $arguments;
# Write-Output $oResult.stdout;
# Write-Host $oResult.stdout;
$orientation = Write-Output $oResult.stdout;
Add-Type -AssemblyName System.Drawing;
$rotateFlipType = [System.Drawing.RotateFlipType]::Rotate90FlipNone;
Write-Output $orientation;
Write-Host ("{0}: aaaaaaaaaaaaaaaaaaaaaaaaaaaaa`n" -f $MyInvocation.MyCommand.Name);
$clockwisedStr = "Rotate 90 CW";
if ($orientation -eq $clockwisedStr) {
Write-Host ("{0}: 90 degree clockwised." -f $inputPath);
}
elseif ($orientation -eq "Rotate 180") {
$rotateFlipType = [System.Drawing.RotateFlipType]::Rotate180FlipNone;
Write-Host ("{0}: 180 degree clockwised." -f $inputPath);
}
elseif ($orientation -eq "-") {
Write-Host ("{0}: unclockwised." -f $inputPath);
return $false;
}
elseif ($orientation -eq "Unknown (0)") {
Write-Host ("{0}: this file skipping." -f $inputPath);
return $false;
}
else {
Write-Host ("{0}: Unknown orientation value: '{1}'" -f $inputPath, $orientation);
return $false;
}
Write-Host ("{0}: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb`n" -f $MyInvocation.MyCommand.Name);
Write-Output $inputPath;
Write-Output $outputPath;
Write-Output $rotateFlipType;
Write-Host ("{0}: ccccccccccccccccccccccccccccccccccc`n" -f $MyInvocation.MyCommand.Name);
$img = [System.Drawing.Image]::FromFile($inputPath);
$img.RotateFlip($rotateFlipType); ###############################
$img.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Jpeg);
$img.Dispose();
Write-Host ("{0}: dddddddddddddddddddddddddddddddddd`n" -f $MyInvocation.MyCommand.Name);
function Get-FileName([string]$absPath) {
$path = (Resolve-Path -Path $absPath -Relative);
$item = (Get-Item -Path (Resolve-Path -Path $path -Relative));
$baseName = $item.BaseName;
$ext = $item.Extension;
$fileName = "{0}{1}" -f $baseName, $ext;
return $fileName;
}
# Rename a source file and a destination file.
$orgFileName = (Get-FileName $inputPath);
# $newFileName = (Get-FileName $outputPath);
Write-Output $inputPath;
Write-Output $outputPath;
Write-Output $suffix;
Write-Host ("{0}: ffffffffffffffffffffffffffffffffff`n" -f $MyInvocation.MyCommand.Name);
Get-Item -Path (Resolve-Path -Path $inputPath -Relative) | ForEach-Object {
Write-Host ("{0}: hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh`n" -f $MyInvocation.MyCommand.Name);
# $global:orgBaseName = $_.BaseName;
$newName = $_.BaseName + $suffix + $_.Extension;
Rename-Item -Path $_.FullName -NewName $newName;
};
Get-Item -Path (Resolve-Path -Path $outputPath -Relative) | ForEach-Object {
Write-Host ("{0}: iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii`n" -f $MyInvocation.MyCommand.Name);
$newName = $orgFileName;
Rename-Item -Path $_.FullName -NewName $newName;
};
Write-Output $orgFileName;
Write-Host ("{0}: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`n" -f $MyInvocation.MyCommand.Name);
return $true;
}
$suffix = "_org";
Set-ImageOrientations $suffix;
$tmp = Read-Host "Input 'y' if you wanna move original images......";
$suffixWithExt = ".\*{0}.jpg" -f $suffix;
If ($tmp -eq "y") {
# Move-Item -Path .\*_org.jpg -Destination .\5_original_files;
Move-Item -Path $suffixWithExt -Destination .\5_original_files;
}
ざっくり言うと、Set-ImageOrientation
で1枚の画像ファイルを回転させて、Set-ImageOrientations
でを複数回実行させます。
処理の流れとしては、こんな感じです。
Set-ImageOrientations
で、ファイルを複数個取得して、各々のファイルに対してSet-ImageOrientation
を実行。Set-ImageOrientation
で、「Exiftool」ライブラリを使ってどれぐらい回転しているかの情報を取得。画像を回転させる必要があるかどうかを判断する。不要な場合はこの関数が終了して次の画像の判断に移る。- 画像を回転させる場合は、アセンブリから
System.Drawing
名前空間を呼び出して、画像を回転させる。 - 元の画像と修正後の画像のファイル名をリネームする。(その後のWebpに変換する作業を楽にするためです。)
留意点が2つあり、1つ目は「Exiftool」というライブラリを使っている点です。このライブラリは、画像ファイルの中で「Exif」という(その他にも色々な)形式で保持されているメタデータに参照できるものです。このライブラリに関しては後述します。
2つ目は、画像が90度回転しているのか、180度回転しているのか等といった情報を取得するために、Exiftool実行時の標準出力をRegister-ObjectEvent
コマンドレットで取得している点です。Start-Process
コマンドレットを使用する方法もありますが、それだとテキストファイル等にI/Oする必要がありますが、今回使った方法を使えばメモリ上で完結することが出来ます。詳しいことは以下の記事をご参照下さい。
その処理内容は貼っておきます。そのRegister-ObjectEvent
コマンドレットを使用してExiftoolの標準出力を取得している、Invoke-ExternalCommandの関数の内容は以下のとおりです。
function Invoke-ExternalCommand([string]$commandPath, [string]$arguments) {
try {
# Creating process object.
$pinfo = New-Object System.Diagnostics.Process
# Setting process invocation parameters.
$pinfo.StartInfo.FileName = $commandPath
$pinfo.StartInfo.Arguments = $arguments
$pinfo.StartInfo.UseShellExecute = $false
$pinfo.StartInfo.CreateNoWindow = $true
$pinfo.StartInfo.UseShellExecute = $false
$pinfo.StartInfo.RedirectStandardOutput = $true
$pinfo.StartInfo.RedirectStandardError = $true
# Async Writing: Start
# Creating string builders to store stdout and stderr.
$oStdOutBuilder = New-Object -TypeName System.Text.StringBuilder
$oStdErrBuilder = New-Object -TypeName System.Text.StringBuilder
# Adding event handers for stdout and stderr.
$sScripBlock = {
if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
$Event.MessageData.AppendLine($EventArgs.Data)
}
}
$oStdOutEvent = Register-ObjectEvent -InputObject $pinfo `
-Action $sScripBlock -EventName 'OutputDataReceived' `
-MessageData $oStdOutBuilder
$oStdErrEvent = Register-ObjectEvent -InputObject $pinfo `
-Action $sScripBlock -EventName 'ErrorDataReceived' `
-MessageData $oStdErrBuilder
# Starting process.
[Void]$pinfo.Start()
$pinfo.BeginOutputReadLine()
$pinfo.BeginErrorReadLine()
[Void]$pinfo.WaitForExit()
# Async Writing: End
# Unregistering events to retrieve process output.
Unregister-Event -SourceIdentifier $oStdOutEvent.Name
Unregister-Event -SourceIdentifier $oStdErrEvent.Name
$oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
"ExitCode" = $pinfo.ExitCode;
"stdout" = $oStdOutBuilder.ToString().Trim();
"stderr" = $oStdErrBuilder.ToString().Trim();
})
return $oResult
}
finally {
$pinfo.Dispose()
}
}
【余談】Exiftoolを使ってタグをいじる方法も試した。
最初は、「Exiftool」を使って回転させられないかを試しました。
Exiftoolのリファレンスはこちらで確認できます。まあちょっと情報が膨大で全て見通すのは骨が折れます。
Windows上では回転するのに、Web上では回転しない。
そのサイトからダウンロードできる「Exiftool.exe」をローカルフォルダに配置して、以下のコマンドをPowerShellで実行します。そうすると、ソース画像の回転情報が変換後の画像の回転情報に反映されます。
.\exiftool.exe "-tagsFromFile" <source image file> "-orientation<orientation" <destination image file>
# for example
.\exiftool.exe "-tagsFromFile" .\20220729234626.jpg "-orientation<orientation" .\20220729234626.webp
90度に回転させるようにしたので、Webp拡張子でも縦向きを保持するようになると思ったのですが・・・アレッ・・・?
しかし・・・、Web上にアップロードすると横向きになってしまいました・・・。
これはMEGA上に上げた時の写真です。Windows上では縦向きに戻ったのに・・・。
今回は失敗だが、ついでにExiftoolの使い方をチラっと。
ちなみに、画像の回転情報以外に、全ての情報を反映したい場合は、以下のコマンドを実行します。
.\exiftool.exe "-tagsFromFile" <source image file> "-exif:all" <destination image file>
# for example
.\exiftool.exe "-tagsFromFile" .\20220729234626.jpg "-exif:all" .\20220729234626.webp
基本的には、この"-exif:all"
という文字列を引数に入れれば、元画像のタグにある情報はほぼ反映できます。
しかしながら、個別のタグにある情報のみを反映したい場合は、以下のコマンドで"-exif:all"
で情報を反映した後の画像がどういう名前のタグを持っているかを調べてから、個別に反映出来るかどうかが判断出来ます。
拡張子によって、保持できるタグの種類は異なるので調べる必要があります。
.\exiftool -s .\20220729234626.webp
実際のタグ名を併せてこっちを実行すると、タグの値が表示されます。
.\exiftool -T "-Orientation" .\20220729234626.webp
例えばこのように出力されます。
# 画像を回転させる前などの出力の一部("-exif:all")
...
ImageWidth : 2280
ImageHeight : 1080
...
# 画像を回転させた後などの出力の一部("-exif:all")
...
ImageWidth : 1080
ImageHeight : 2280
...
# 画像を回転させる前などの出力の一部("-Orientation"が90度)
Rotate 90 CW
# 画像を回転させる前などの出力の一部("-Orientation"が180度)
Rotate 180
まとめ
PowerShellでExifを編集して画像の向きを戻す方法を紹介しました。
本記事のまとめです。
- WebP形式に変換後の画像が正しい向きで表示されない問題
- PowerShellスクリプトで単体の画像を回転する方法(
System.Drawing.Image
名前空間を使う。) - 複数枚の画像を一括回転する方法(
Register-ObjectEvent
コマンドレットを使う。) - 回転情報が異なる場合の条件分岐の方法(Exiftoolライブラリを使う。)
- Exiftoolを使ったタグ編集とその留意点
Exiftoolに関する記事は過去にも書きました。良ければ見てみて下さい。
おしまい
よし、これで画像を整理できる!
ちなみに、画像の向きを変えるよりも、
ファイルパスの操作が一番面倒だった・・・。
以上になります!
コメント