Visual Basic 中学校 掲示板 投稿内容
タグのない投稿を抽出 統計 RSS

Visual Basic 中学校 > 投稿一覧 >

壊れている画像ファイルの読み込みを避けるには 解決済み

タグの編集...

投稿者 ねぼすけ   (社会人)   投稿日時 2016/8/8 07:53:54
 フォルダ内の画像(写真[jpg])を読み込み、リサイズ・リネーム後フォルダ内に新しく作成されたフォルダ内に保存するプログラムを作りました。
 目的を達成できた場合とできなかった場合がありました。
 よく調べてみると、壊れているファイルを読み込んだときに、それ以降はリサイズされていない感じです。(リネームはされている。)
 壊れているファイルを削除してプログラムを実行すると目的を達成することができました。
 壊れているファイルはWindowsのエクスプローラーでのサムネイルは「山の景色マーク」(?)です。また「PhotoShop」で開いてみると、『「….jpg」を開くことができません。不明または無効なJPEGマーカーが見つかりました。』と表示されます。
 このファイル、あるいは壊れているファイルの読み込みを避けるにはどうすれば良いのでしょう。宜しくお願いいたします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2016/8/8 09:24:43
> 壊れているファイルの読み込みを避けるには
読み込まないと、壊れているかどうか判断できませんよ。


> それ以降はリサイズされていない感じです。
一般論としては、「処理が失敗するかどうか」で
例外処理を設けるのが妥当かと思います。

ただ、『それ以降』が失敗するという点が気にかかりました。


> リサイズ・リネーム後
リネーム・リサイズを行っているのではなく、
リサイズ・リネームを行っているのですよね。

ということは、現時点で例外処理はできており、だからこそ
後続のリネーム処理が継続されているように読み取れました。

ただ質問内容からは、その例外処理が適切な記述に
なっているのかどうかは読み取れませんでしたが…。


・リサイズ処理は、どのような記述で行っているのでしょうか。
 Image.GetThumbnailImage でしょうか。
 Graphics.DrawImage でしょうか。
 New TransformedBitmap(BitmapSource, Transform) でしょうか。

・画像のリサイズに失敗した時、
 何らかのエラー(Exception)は発生しますか?
 それともエラーは発生しない代わりに
 『それ以降』の処理が期待動作しなくなるのでしょうか?


投稿者 ねぼすけ   (社会人)   投稿日時 2016/8/8 11:17:22
プログラムの流れは
①呼び出したフォルダを得る
②ファイルの最初の文字列を得る
③最初の文字列にランダムに発生させた連番をつける
 nebosuke0001.jpgというような感じ
④ターゲットフォルダ内に「Temp」フォルダを作成
 し、名前を付け替えて保存
⑤ファイルを呼び出し、縮小
⑥同じファイル名で保存
③~⑥はBackgroundWorkerで処理しています。
稚拙ですがそのコードは
Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Dim i As Integer
        Dim fl As Integer = Files.Length
        '連番の作成 
        Dim Numbers(fl - 1) As Integer
        For i = 0 To fl - 1
            Numbers(i) = i
        Next
        Randomize()
        Dim n As Integer = fl - 1
        Dim r As Integer
        Dim dmy As Integer
        For i = n To 0 Step -1
            r = Int(n * (Rnd()))
            dmy = Numbers(n)
            Numbers(n) = Numbers(r)
            Numbers(r) = dmy
        Next
        '先頭のファイル名を得る 
        Dim FirstName As String = txtHeader.Text
        'ターゲットフォルダ内に「Temp」フォルダを作成 
        IO.Directory.CreateDirectory(TargetPath & "\Temp")
        '「Temp」フォルダにリネームされたファイルを保存し、 
        'ListBoxに変更されたファイル名を表示するため変更の様子をNewFilesに格納 
        Dim NewFileName As String
        For i = 0 To fl - 1
            NewFileName = TargetPath & "\Temp" & "\" & FirstName & Numbers(i).ToString("D4") & ".jpg"
            'IO.File.Move(Files(i), NewFileName) 
            IO.File.Copy(Files(i), NewFileName)
            '元のファイル名と変更されたファイル名を格納 
            NewFiles.Add(Files(i) & " → " & TargetPath & "\Temp" & "\" & FirstName & Numbers(i).ToString("D4") & ".jpg")
        Next
        '■画像を縮小 
        Const DisplayWidth As Integer = 1024
        Const DisplayHeight As Integer = 768
        '「TargetPath & "\Temp"」から、ファイルを読み込む 
        Files = System.IO.Directory.GetFiles(TargetPath & "\Temp" & "\""*.jpg")
        Dim count As Integer = 0
        For Each f As String In Files
            '画像を読み込む 
            Dim img As Image = Image.FromFile(f)
            '縮小割合を求める 
            Dim rate As Single
            If img.Width > img.Height Then
                Rate = DisplayWidth / img.Width
            Else
                Rate = DisplayHeight / img.Height
            End If
            'リサイズする横、縦を求める 
            Dim reSizeWidth As Integer = img.Width * rate
            Dim reSizeHeight As Integer = img.Height * rate
            '描画先とするImageオブジェクトを作成する 
            Dim canvas As New Bitmap(reSizeWidth, reSizeHeight)
            'ImageオブジェクトのGraphicsオブジェクトを作成する 
            Dim g As Graphics = Graphics.FromImage(canvas)
            '画像をリサイズしてcanvasに描画する 
            g.DrawImage(img, 0, 0, reSizeWidth, reSizeHeight)
            'Imageオブジェクトのリソースを解放する 
            img.Dispose()
            'Graphicsオブジェクトのリソースを解放する 
            g.Dispose()
            '画像を同じ名前で保存する 
            ' canvas.Save(f)'Save(f)だけでは拡張子が「jpg」であってもjpgでは保存されない 
            canvas.Save(f, ImageFormat.Jpeg)
            canvas.Dispose()
            '進行状況を表示 
            count += 1
            Dim percent As Integer = count / Files.Length * 100
            BackgroundWorker1.ReportProgress(percent)
        Next
End Sub



>・画像のリサイズに失敗した時、
>  何らかのエラー(Exception)は発生しますか?
> それともエラーは発生しない代わりに
> 『それ以降』の処理が期待動作しなくなるのでしょうか?
エラーは発生しないのです。それとも、まだ確かめていませんが、BackgoundWorker
はえらーしてもそのまま続行ですか。

>ということは、現時点で例外処理はできており、だからこそ
>後続のリネーム処理が継続されているように読み取れました。
>ただ質問内容からは、その例外処理が適切な記述に
>なっているのかどうかは読み取れませんでしたが…。
例外処理はしていないのです。たかだか自分の写真なので
まさか、このようになるとは思わなかったので…。

>リサイズ処理…
Graphics.DrawImageかな?


投稿者 ねぼすけ   (社会人)   投稿日時 2016/8/8 12:45:56
「Temp」フォルダにはリサイズする前にリネームして保存しているので、BackgroundWorkerで処理が中断してもファイルは残りますよね。では、きっとエラーをしているのでしょう。

投稿者 まりもん   (社会人)   投稿日時 2016/8/8 14:04:28
プログラムが正常動作した結果、エラーとなるファイル以降がリサイズされなかったのではなく
エラーとなるファイルを処理しようとした結果、例外が発生し処理が途中で終わっているのではないですか?

例外処理を入れてみてはどうでしょうか

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2016/8/8 15:43:55
> ③最初の文字列にランダムに発生させた連番をつける

「0~(Files.Length - 1)」という範囲の被らない乱数表が必要な場合、下記の一行で生成できます。

Dim Numbers() As Integer = Enumerable.Range(0, Files.Length).OrderBy(AddressOf Guid.NewGuid).ToArray()



> Dim FirstName As String = txtHeader.Text
BackgroundWorker の中で、TextBox 等を読み書きしてはいけません。
データの受け渡しは、イベント引数を通じて行って下さい。

UI スレッドで作成したコントロールを操作できるのは UI スレッドだけです。
もしもワーカースレッドから操作した場合、処理内容によっては
 『コントロールが作成されたスレッド以外のスレッドから
  コントロール 'txtHeader' がアクセスされました。』
といった例外が発生するなど、予期せぬ動作を引き起こす可能性があります。


コントロールの内容をワーカースレッドに渡す必要がある場合は、
 呼び出し元:BackgroundWorker1.RunWorkerAsync(txtHeader.Text)
 ワーカー側:Dim FirstName As String = CStr(e. Argument)
のようにしましょう。これなら大丈夫です。



同様に、マルチスレッドという観点からすると、
Files というフィールド変数の共有も、本来は望ましくありません。
(配列の操作はスレッドセーフではありません)

ただし、ワーカースレッドの動作中は、Files の内容が書き換わる事が
絶対に無いと保証できているのであれば、問題なく動作します。
しかしコントロールやフォームの操作は、たとえ読み取りのみであったとしても御法度です。


> NewFiles.Add(Files(i) & " → " & TargetPath & "\Temp" & "\" & FirstName & Numbers(i).ToString("D4") & ".jpg")
これも同様にマズイです。

呼び出し元のスレッドとワーカースレッドとで、変数の共有は御法度です。
データの受け渡しは、イベント引数を通じて行って下さい。


ワーカースレッドでの作業結果を呼び出し元に伝えるには、
DoWork イベント内で
 Dim results As New List(Of String)()
 results.Add(…)
 results.Add(…)
 e.Result = results
のように、e.Result に渡すようにし、それを呼び出し元が
RunWorkerCompleted イベント内で
 NewFiles = DirectCast(e.Result, List(Of String))
のように、e.Result を通じて行います。


もしも進捗表示として使いたいのであれば、ReportProgress の第二引数に
渡すようにして、ProgressChanged の e.UserState で受け取ります。



> '■画像を縮小 
> Const DisplayWidth As Integer = 1024
> Const DisplayHeight As Integer = 768
この場合、400x300 の画像だったら、拡大になってしまいますが、大丈夫でしょうか?


> '縮小割合を求める 
> Dim rate As Single
> If img.Width > img.Height Then
>  Rate = DisplayWidth / img.Width

「DisplayWidth / img.Width」が Single 型を返すのは、
DisplayWidth が As Single だった場合だけです。

DisplayWidth が As Integer だった場合には、
「DisplayWidth / img.Width」は Double 型になるのでご注意下さい。



> 'リサイズする横、縦を求める 
> Dim reSizeWidth As Integer = img.Width * rate
両辺の型をあわせるために、= CInt(img.Width * rate) にした方が良いですね。
切上げ・切捨て・丸めを明示したい場合は、Math クラスの
Ciling・Floor・Round メソッドを併用する事も出来ます。


> エラーは発生しないのです。それとも、まだ確かめていませんが、BackgoundWorker
> はえらーしてもそのまま続行ですか。
BackgroundWorker 内で例外が発生した場合、それを Catch していなければ
残りの処理は中断されます。しかし、呼び出し元がエラーになることはありません。

BackgroundWorker 内で例外が発生したかどうかを確認するには、
RunWorkerCompleted イベント内で、e.Error を確認します。
これが Nothing であればエラーは起きていないことになります。

なお、CancelAsync メソッドを併用している場合には、
e.Error → e.Cancelled → e.Result の順で確認してみてください。

投稿者 ねぼすけ   (社会人)   投稿日時 2016/8/9 06:43:28
 自分の作ったプログラムなのに…、リネームして「Temp」フォルダに保存、その後リサイズなので、リサイズしなかったのではなく壊れたファイルまで通常に処理、壊れたファイルの処理でBackgroundWorkerは中断したということだと思います。
>RunWorkerCompleted イベント内で、e.Error を確認します。
ということで、MsgBox(e.Error.Message)でいいのでしょうか?確認してみたら「メモリが不足しています。」と表示されました。壊れたファイルの処理中にエラーしたようです。
 そのため、「まりもん」さんのおっしゃるとおり、例外処理をほどこしましたら、壊れたファイル以外はすべてリサイズされました。ありがとうございました。
 「魔界の仮面弁士」さん、「e.Error」や「ワーカースレッドでの作業結果を呼び出し元に伝える…」など、「BackgroundWorker」で知らないことが目白押しでした。「変数の型」も含め、勉強になりました。