xlogI125’s blog

パソコン作業を効率化したい

ビットマップファイルの解像度を変更

ピクセル数を変更せずにパソコン標準ソフトで簡単にビットマップファイル(*.bmp)の解像度を変更する方法が思いつかない。
とりあえずPowerShellにてBitmap.SetResolutionメソッドなどを用いて解像度を変更する方法を考える。

使い捨てスクリプト (Windows ターミナル に貼り付け)

# PowerShell 5.1, Windows 11 (2024年6月頃)

$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
Add-Type -AssemblyName Microsoft.VisualBasic, System.Drawing, System.Windows.Forms

Invoke-Command -NoNewScope -ScriptBlock {
  # OpenFileDialog
  $dlg = [System.Windows.Forms.OpenFileDialog]::new()
  $dlg.Filter = "Image (*.bmp;*.tif;*.jpg;*.png)|*.bmp;*.tif;*.jpg;*.png|All Files|*.*"
  $dlg.Title = "入力用 画像ファイル"
  $dlg.InitialDirectory = "$env:USERPROFILE\Desktop"
  if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) {
    Write-Host "ファイルの選択がキャンセルされました"; return
  }

  # 入力用 ファイルパス
  $inFilePath = $dlg.FileName

  # 画像ファイルを開く
  $bitmap = [System.Drawing.Bitmap]::new($inFilePath)

  if ($bitmap.HorizontalResolution -ne $bitmap.VerticalResolution) {
    Write-Host "HorizontalResolution と VerticalResolution が異なります"; return
  }

  # 解像度などを確認(元の画像ファイル)
  $bitmap | Format-List -Property @(
    @{ Name = "FileName"; Expr = { $inFilePath } }, 
    "PixelFormat", "Width", "Height", "HorizontalResolution", "VerticalResolution", 
    @{ Name = "Xmm"; Expr = { $_.Width / $_.HorizontalResolution * 25.4 } }, 
    @{ Name = "Ymm"; Expr = { $_.Height / $_.VerticalResolution * 25.4 } }
  )

  # 解像度(変更後)
  $newDpi = [double]::Parse([Microsoft.VisualBasic.Interaction]::InputBox(
    "短辺5mm以上となる解像度(dpi)を入力", "解像度(変更後)", $bitmap.HorizontalResolution.ToString()
  ))

  # 出力用 bmpファイルパス
  $outFilePath = "$env:USERPROFILE\Desktop\" + 
  [System.IO.Path]::GetFileNameWithoutExtension($inFilePath) + "_" + 
  $newDpi.ToString() + "_" + [System.DateTime]::Now.ToString("HHmmss") + ".bmp"

  if (Test-Path -LiteralPath $outFilePath) {
    Write-Host "出力先にファイルが既に存在しています`n$($outFilePath)"; return
  }

  # 解像度を設定
  $bitmap.SetResolution($newDpi, $newDpi)

  # 保存
  $bitmap.Save($outFilePath, [System.Drawing.Imaging.ImageFormat]::Bmp)
  $bitmap.Dispose()

  # 保存したbmpファイルを開く
  $bitmap = [System.Drawing.Bitmap]::new($outFilePath)

  # 解像度などを確認(保存したbmpファイル)
  $bitmap | Format-List -Property @(
    @{ Name = "FileName"; Expr = { $outFilePath } }, 
    "PixelFormat", "Width", "Height", "HorizontalResolution", "VerticalResolution", 
    @{ Name = "Xmm"; Expr = { $_.Width / $_.HorizontalResolution * 25.4 } }, 
    @{ Name = "Ymm"; Expr = { $_.Height / $_.VerticalResolution * 25.4 } }
  )
  $bitmap.Dispose()
}

使い捨てスクリプト (スクリプトファイルとショートカット)

# [テスト環境]
#  PowerShell 5.1, Windows 11 (2024年6月頃)
# [スクリプトファイル(.ps1)]
#  <ファイル名>
#   "%USERPROFILE%\Desktop\TestSetResolution.ps1"
#  <エンコード>
#   UTF-8 (BOM付き)
# [ショートカット(.lnk)]
#  <リンク先(T)>
#   PowerShell.exe -NoLogo -NoExit -NoProfile -ExecutionPolicy RemoteSigned -File "%USERPROFILE%\Desktop\TestSetResolution.ps1"
#  <作業フォルダー(S)>
#   ""

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
$VerbosePreference = [System.Management.Automation.ActionPreference]::Continue

Set-StrictMode -Version Latest

Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName Microsoft.VisualBasic

Write-Verbose $MyInvocation.InvocationName

# 入力用 ファイルパス
$inFilePath = & {
  $dlg = [Microsoft.Win32.OpenFileDialog]::new()
  $dlg.Filter = "Bitmap (*.bmp)|*.bmp|All Files|*.*"
  $dlg.Title = "入力用 bmpファイル"
  $dlg.InitialDirectory = "$env:USERPROFILE\Desktop"
  if ($args.Length -ne 0) {
    $dlg.InitialDirectory = (Split-Path -LiteralPath $args[0])
    $dlg.FileName = (Split-Path -Leaf $args[0])
  }
  if ($dlg.ShowDialog() -eq $false) {
    Write-Error "ファイルの選択がキャンセルされました"
  }
  $dlg.FileName  # ファイルパス
} @args  # Splatting

Set-Location -LiteralPath (Split-Path -LiteralPath $inFilePath)

# 入力用 ファイルを開く
$inFileStream = [System.IO.FileStream]::new($inFilePath, [System.IO.FileMode]::Open)

# 入力用 BitmapFrame
$inBitmapFrame = [System.Windows.Media.Imaging.BmpBitmapDecoder]::new(
  $inFileStream, 
  [System.Windows.Media.Imaging.BitmapCreateOptions]::PreservePixelFormat, 
  [System.Windows.Media.Imaging.BitmapCacheOption]::Default
).Frames[0]

if ($inBitmapFrame.DpiX -ne $inBitmapFrame.DpiX) {
  Write-Error "DpiX と DpiY が異なります"
}

# 短辺が5mmとなる解像度
$maxDpi = [Math]::Min($inBitmapFrame.PixelWidth * 25.4 / 5, $inBitmapFrame.PixelHeight * 25.4 / 5)

$newDpi = $inBitmapFrame.DpiX
$mmX = $inBitmapFrame.PixelWidth / $newDpi * 25.4
$mmY = $inBitmapFrame.PixelHeight / $newDpi * 25.4

# 解像度(変更後)を入力
do {
  [PSCustomObject]@{
    "解像度(短辺5mm)[dpi]" = $maxDpi; "解像度[dpi]" = $newDpi; "幅[mm]" = $mmX; "高さ[mm]" = $mmY
  } | Format-List
  $newDpi = [double]::Parse([Microsoft.VisualBasic.Interaction]::InputBox(
    "短辺5mm以上となる解像度(dpi)を入力", "解像度(変更後)", $inBitmapFrame.DpiX.ToString()
  ))
  $mmX = $inBitmapFrame.PixelWidth / $newDpi * 25.4
  $mmY = $inBitmapFrame.PixelHeight / $newDpi * 25.4
} while (($mmX -lt 5.0) -or ($mmY -lt 5.0))

# 出力用 ファイルパス
$outFilePath = & {
  $path = "$env:USERPROFILE\Desktop\" + 
  [System.IO.Path]::GetFileNameWithoutExtension($inFilePath) + "_" + 
  $newDpi.ToString() + "_" + [System.DateTime]::Now.ToString("HHmmss") + ".bmp"

  if (Test-Path -LiteralPath $path) {
    Write-Error "出力先にファイルが既に存在しています`n$($path)"
  }
  $path
}

<# メモ 切り上げ
[Math]::Round(24 * 600 * (841 / 25.4), 1)  #=> 476787.4
& { for ($i = 0; $i -le 476788; $i++) { [int][Math]::Truncate(($i + 7) / 8) -eq [int][Math]::Ceiling($i / 8) } } | Sort-Object | Get-Unique  #=> True
#>

# 出力用 BitmapFrame
$outBitmapFrame = & {
  param([System.Windows.Media.Imaging.BitmapFrame]$bitmapFrame, [double]$dpi)
  $stride = [int][System.Math]::Truncate(($bitmapFrame.Format.BitsPerPixel * $bitmapFrame.PixelWidth + 7) / 8)
  $pixels = [byte[]]::new($stride * $bitmapFrame.PixelHeight)
  $bitmapFrame.CopyPixels($pixels, $stride, 0)
  $bitmapSource = [System.Windows.Media.Imaging.BitmapSource]::Create(
    $bitmapFrame.PixelWidth, $bitmapFrame.PixelHeight, # ピクセル単位
    $dpi, $dpi, # 解像度(dpi)
    $bitmapFrame.Format, $bitmapFrame.Palette, $pixels, $stride
  )
  [System.Windows.Media.Imaging.BitmapFrame]::Create($bitmapSource)
} -bitmapFrame $inBitmapFrame -dpi $newDpi

# 保存
& {
  param([System.Windows.Media.Imaging.BitmapFrame]$bitmapFrame, [string]$filePath)
  $bmpBitmapEncoder = [System.Windows.Media.Imaging.BmpBitmapEncoder]::new()
  $bmpBitmapEncoder.Frames.Add($bitmapFrame)
  $fileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::CreateNew)
  $bmpBitmapEncoder.Save($fileStream)
  $fileStream.Dispose()
} -bitmapFrame $outBitmapFrame -filePath $outFilePath

# 保存したbmpファイルを開く
$outFileStream = [System.IO.FileStream]::new($outFilePath, [System.IO.FileMode]::Open)

$outBitmapFrame = [System.Windows.Media.Imaging.BmpBitmapDecoder]::new(
  $outfileStream, 
  [System.Windows.Media.Imaging.BitmapCreateOptions]::PreservePixelFormat, 
  [System.Windows.Media.Imaging.BitmapCacheOption]::Default
).Frames[0]

# 解像度などを確認
@($inBitmapFrame, $outBitmapFrame) | Format-Table -AutoSize -Property @(
  "Format", "PixelWidth", "PixelHeight", "DpiX", "DpiY", 
  @{ Name = "Xmm"; Expr = { $_.PixelWidth / $_.DpiX * 25.4 } }, 
  @{ Name = "Ymm"; Expr = { $_.PixelHeight / $_.DpiY * 25.4 } }
)

$inFileStream.Dispose()
$outFileStream.Dispose()