亀の甲羅2

今日もまた朝とく起きて励まなん窓に明るきありあけの月

PowerToys text-extractorに韓国語のOCRパッケージをインストールする。

ちょっと韓国の情報が気になりtext-extractorで画像から文字を取得したい場合が出てきた。

learn.microsoft.com

管理者権限のあるpowershellで以下を実行する。

$Capability = Get-WindowsCapability -Online | Where-Object { $_.Name -Like 'Language.OCR*ko-KR*' }
$Capability | Add-WindowsCapability -Online

text-extractorの優先する言語にハングル(韓国語)が出てくる。

あとはいつも通り、使うだけ。

poweshell 入力フィールドを持つGUIウィンドウを作成する(Bing)

Bingに作らせてみた。BingAIは指示代名詞が使えるので、簡単な質問から始めて徐々に深化させて要求する答えを導くことができる。なかなか使える。

  1. powershellGUIウィンドウを表示するスクリプト
  2. サンプルスクリプト
  3. 上記に入力フィールドとOKボタンを追加したスクリプトを作成して下さい。
  4. 上記を入力内容を返却する関数に修正して下さい。
  5. 上記にキャンセルボタンを追加して下さい。キャンセルボタンを押下したときは、ウィンドウを閉じてください。
  6. 上記のスクリプトで、ウィンドウサイズ変更不可にして下さい。
  7. 上記で最小化ボタンと閉じるボタン(×ボタン)も消してください。
  8. 上記でEnter押下したときにOKボタンを作動させてください。
  9. 上記で「入力してください」というラベルを加えて下さい。
  10. 上記で「入力してください」と「【お知らせ】」の部分を引数として与えられるように改変して下さい。

というプロンプトを経てできたスクリプトが下記の通り。

function Show-InputBox ([string] $FormTitle = "【お知らせ】", [string] $InputLabel = "入力してください") {
    # アセンブリの読み込み
    [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
    [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

    # オブジェクトを生成
    $Form = New-Object System.Windows.Forms.Form

    # フォームのサイズ
    $Form.Size = New-Object System.Drawing.Size(400, 300)

    # フォームの表示場所
    $Form.Location = New-Object System.Drawing.Size(1500, 700)

    # フォームの表示設定
    $Form.StartPosition = "manual"

    # フォームのテキスト
    $Form.Text = $FormTitle

    # ウィンドウサイズ変更不可にする
    $Form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
    $Form.MaximizeBox = $false

    # 最小化ボタンと閉じるボタン(×ボタン)を消す
    $Form.ControlBox = $false

    # 入力フィールドを追加
    $InputBox = New-Object System.Windows.Forms.TextBox
    $InputBox.Location = New-Object System.Drawing.Point(10, 60)
    $InputBox.Size = New-Object System.Drawing.Size(360, 20)
    $InputBox.Add_KeyDown({
        if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
            $Form.DialogResult = [System.Windows.Forms.DialogResult]::OK
            $Form.Close()
        }
    })
    $Form.Controls.Add($InputBox)

    # 入力してくださいというラベルを追加
    $InputLabelControl = New-Object System.Windows.Forms.Label
    $InputLabelControl.Location = New-Object System.Drawing.Point(10, 40)
    $InputLabelControl.Size = New-Object System.Drawing.Size(360, 20)
    $InputLabelControl.Text = $InputLabel
    $Form.Controls.Add($InputLabelControl)

    # OKボタンを追加
    $OKButton = New-Object System.Windows.Forms.Button
    $OKButton.Location = New-Object System.Drawing.Point(120, 100)
    $OKButton.Size = New-Object System.Drawing.Size(75, 23)
    $OKButton.Text = "OK"
    $OKButton.Add_Click({
        $Form.DialogResult = [System.Windows.Forms.DialogResult]::OK
        $Form.Close()
    })
    $Form.Controls.Add($OKButton)

    # キャンセルボタンを追加
    $CancelButton = New-Object System.Windows.Forms.Button
    $CancelButton.Location = New-Object System.Drawing.Point(200, 100)
    $CancelButton.Size = New-Object System.Drawing.Size(75, 23)
    $CancelButton.Text = "キャンセル"
    $CancelButton.Add_Click({
        $Form.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
        $Form.Close()
    })
    $Form.Controls.Add($CancelButton)

    # フォームを表示
    $Form.Add_Shown({$Form.Activate()})

    # フォームをアクティブ
    if ($Form.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
        return $InputBox.Text
    } else {
        return ""
    }
}

# 関数を呼び出して入力内容を取得する例(引数あり)
$inputText1 = Show-InputBox -FormTitle "【確認】" -InputLabel "名前を入力してください"
Write-Host "入力内容1: $inputText1"

# 関数を呼び出して入力内容を取得する例(引数なし)
$inputText2 = Show-InputBox
Write-Host "入力内容2: $inputText2"

Bingちゃんの説明は下記の通り。

このスクリプトは、Show-InputBoxという関数を定義しています。この関数は、GUIウィンドウを表示し、入力フィールドとOKボタン、キャンセルボタンが配置されています。OKボタンをクリックすると、入力フィールドに入力されたテキストが返却されます。キャンセルボタンをクリックすると、ウィンドウが閉じられます。

Enterキーを押下したときにOKボタンを作動させるために、$InputBoxオブジェクトにKeyDownイベントハンドラーを追加しています。このイベントハンドラーでは、押下されたキーがEnterキーかどうか判定し、Enterキーであれば、フォームのDialogResultプロパティをOKに設定して、フォームを閉じています。

関数を呼び出すことで、入力内容を取得することができます。

「入力してください」というラベルを追加するために、$InputLabelオブジェクトを作成し、LocationプロパティとSizeプロパティ、Textプロパティを設定しています。そして、$InputLabelオブジェクトをフォームのコントロールに追加しています。

このスクリプトは、Show-InputBoxという関数を定義しています。この関数は、$FormTitleパラメーターと$InputLabelパラメーターを受け取ります。これらのパラメーターは、それぞれフォームのテキストとラベルのテキストに使用されます。

関数を呼び出す際に、引数としてフォームのテキストとラベルのテキストを指定することができます。引数が指定されていない場合は、デフォルト値が使用されます。

関数を呼び出すことで、入力内容を取得することができます。


別解としてVisualBasicアセンブリを使ってフォームを作成することもできる。powershell3.0以降で動作するとのこと。
こっちはあまり深追いしなかった。

Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms

$result = [Microsoft.VisualBasic.Interaction]::InputBox("メッセージ", "タイトル", "デフォルト値")

if ($result -ne "") {
    [System.Windows.Forms.MessageBox]::Show($result, "入力された値")
}

powershell 例外処理とexit

通常、powershellでexit 0 が正常終了、0以外が異常終了となる。

終了コードは「%errorlevel%」環境変数に格納されるため、PowerShellスクリプトを実行した後、echo %errorlevel%と記述して終了コードを表示できる。

powershellスクリプト側はこんな感じ。

try {
    # 例外が発生する可能性のあるコード
}
catch {
    # 例外が発生した場合の処理(エラーメッセージを表示してみよう)
    Write-Host "An error occurred: $($_.Exception.Message)"
    exit 1
}

# 正常終了
exit 0

vbs側では、こんな感じ。

'文字コード:cp932 (sjis)

Option Explicit

' エラーが発生するとダイアログが出て処理が終わらない問題対策
On Error Resume Next

Dim objShell
Dim PS1File
Dim Arguments
Dim exitcode 

Set objShell = CreateObject( "WScript.Shell" )

PS1File = "hoge.ps1"
' 必要に応じて引数を与える(日本語の受け渡しは注意)
Arguments = ""

'--------------------------------------------------------------------
 
exitcode = objShell.run ("PowerShell.exe -ExecutionPolicy RemoteSigned -File """ & PS1File & """ " & Arguments, 0, True)
 
set objShell = Nothing

powershell 特定のファイル名のみ削除

これも定型パターンでしょう。
今回は拡張子でパターンを指定してみた。(-Filterはワイルドカード指定っぽい)

$directory = "C:\hogehoge"
Get-ChildItem -Path $directory -Filter *.jpg2 -Recurse | ForEach-Object { Remove-Item -LiteralPath $_.FullName }

別解としては、こんな感じかな。

$path = "C:\hogehoge"
$pattern = ".*.jpg2"
Get-ChildItem -Path $path -Recurse | Where-Object { $_.Name -match $pattern } | Where-Object { !$_.PSIsContainer } | %{Remove-Item -LiteralPath $_.FullName } 

poweshell 特定のパターンに合致しないファイルのみを削除する

もう、これは定型パターンでしょう。

$path = "C:\hogehoge"
$pattern = ".*_noExif.jpg"
Get-ChildItem -Path $path -Recurse | Where-Object { $_.Name -notmatch $pattern } | Where-Object { !$_.PSIsContainer } | %{Remove-Item -LiteralPath $_.FullName } 
  1. Where-Object { $_.Name -notmatch $pattern } でパターンにマッチしないもののみ絞り込みしています。(-notmatch を変更すれば応用は利くはず)
  2. Where-Object { !$_.PSIsContainer } でフォルダを除外しています。

MS Bing にpowershellのスクリプトを作らせてみた

Bing がなかなかできるやつだと聞いたので、ちょっとお試ししてみた。

powershellで画像のExif情報を削除するスクリプトを作成して下さい。」

で、Bingちゃんがくれた回答が下記の通り。まだ動作確認してないけど、直感でこれはイケてるって思えるコード。

param(
    [string]$Path
)

Add-Type -AssemblyName System.Drawing

$image = [System.Drawing.Image]::FromFile($Path)
$propertyItems = $image.PropertyItems

foreach ($item in $propertyItems) {
    $image.RemovePropertyItem($item.Id)
}

$image.Save($Path)
$image.Dispose()

質問の内容によっては、単純なキーワード検索と同じになってしまうこともあるが、
生成させるような問いかけにはなかなか強い感じがする。
知識の集積とその知識の使い方を理解していて、問いかけの意図も理解した上で回答をくれる。
調べる必要がない(英語圏の資料まで読んで知識を集める・・・みたいなこと)ので、楽と言えば楽だが、
考える楽しみまで奪われてしまっているような気持ちになるな。


ちょっと続き。

powershellで画像のExif情報を削除するスクリプトを作成して下さい。Exif情報を削除する処理は関数化して、指定のディレクトリ配下のjpgファイルを再帰的に検索して、その関数でExif情報を削除する形にして下さい。」

で、Bingちゃんがくれた回答が下記の通り。

function Remove-Exif {
    param(
        [string]$Path
    )

    Add-Type -AssemblyName System.Drawing

    $image = [System.Drawing.Image]::FromFile($Path)
    $propertyItems = $image.PropertyItems

    foreach ($item in $propertyItems) {
        $image.RemovePropertyItem($item.Id)
    }

    $image.Save($Path)
    $image.Dispose()
}

$directory = "C:\Images"
Get-ChildItem -Path $directory -Filter *.jpg -Recurse | ForEach-Object {
    Remove-Exif -Path $_.FullName
}

そこら辺の新人よりもまともな回答をよこしやがる。。。


で、動作検証した結果、うまくいかない。

"1" 個の引数を指定して "Save" を呼び出し中に例外が発生しました: "GDI+ で汎用エラーが発生しました。"
発生場所 \Desktop\Exif削除.ps1:18 文字:5
+     $image.Save($Path)
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ExternalException

というエラーが出る。どうやら、Saveが同名上書きができないようだ。

さて、別名を作成してSaveしてみたが、RemovePropertyItem の挙動がおかしい。Exifが削除されない。

以下の方もそのことに気づいており、2014年にブログ化されている。ここでは、下記の方の手法をまねさせていただいた。

[C#] 画像ファイルからExif情報を削除するには – 端くれプログラマの備忘録

自分が手直しした版は下記の通り。
なんかファイルサイズが半分くらいになるので冗長なデータが削除されている?かもしれない。
画像サイズや見た目には影響がないが、Exif以外の部分がバイナリレベルで元ファイルと同一かは保証しかねる。

$directory = "C:\image"

function Remove-Exif {
    param(
        [string]$Path
    )

    Add-Type -AssemblyName System.Drawing

<#
    $image = [System.Drawing.Image]::FromFile($Path)
    $propertyItems = $image.PropertyItems

    $propertyItems = $image.PropertyItems

    foreach ($item in $propertyItems) {
        $image.RemovePropertyItem($item.Id)
    }
#>

    # Bitmapで複製
    $image = New-Object System.Drawing.Bitmap ($path)
    $image2 = New-Object System.Drawing.Bitmap ($image)

    # 複製後のファイル名を作成($pathはStringなのでItemオブジェクトに変換して利用)
    $a = (Get-Item -LiteralPath $Path)
    $path2 = JOIN-PATH $a.DirectoryName ($a.BaseName + "_noExif.jpg")

    # 複製した方を保存(Jpeg指定しないとPNG形式で保存された)
    $image2.Save(($Path2), [System.Drawing.Imaging.ImageFormat]::Jpeg )
    $image.Dispose()
    $image2.Dispose()
}

#--- main ---

Get-ChildItem -Path $directory -Filter *.jpg -Recurse | ForEach-Object {
    Remove-Exif -Path $_.FullName
}