fromfile と filestream について

タグの編集
投稿者 allgreen  (社会人) 投稿日時 2020/6/4 10:59:41
ちょっとまた、面倒なところで行き詰まりました
マルチtifのファイルを、これまで
マルチ画像 = System.Drawing.Image.FromFile(.FileName)
Dim fd As New System.Drawing.Imaging.FrameDimension(マルチ画像.FrameDimensionsList(0))
ページ数 = マルチ画像.GetFrameCount(fd)
ReDim 分割画像(ページ数 - 1)
For i = 0 To ページ数 - 1
     マルチ画像.SelectActiveFrame(fd, i)
     分割画像(i) = マルチ画像.Clone
Next
でやってましたが、この方法だとファイルをロックした状態が終了まで続いて
上書き保存などをするときに書き込めないことになり
ホームページなどを見て
FromFile のところを 
Dim ストリーム As New System.IO.FileStream(.FileName, System.IO.FileMode.Open, System.IO.FileAccess.Read)
マルチ画像 = System.Drawing.Image.FromStream(ストリーム)
に変えることにしました

すると、
g.drawimage などで、GDI+ で汎用エラーが発生しました。のエラーが出るようになりました
.clone を外すとエラーにはなりません(ただし分割画像が全部最後の画像になる)
可能性を考えて、Ctype(マルチ画像.Clone,image)としても駄目でした

ただし、必ずエラーになるというわけではなく、エラーにならない画像ファイルもあります
(今の所、縦長の画像はエラーにならず、横長の画像がエラーになる)

.clone を使わずに、マルチ画像を、分割画像(i)に分離する方法がないか
FileFrom ではエラーにならないのに、FileStream ではエラーになる理由は何か

何かありましたら、お願いします





投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/6/4 11:38:14
> FileFrom ではエラーにならないのに、FileStream ではエラーになる理由は何か

元となった Stream を Close あるいは Dispose してはいませんか?

FileStream メソッドに渡した Stream (MemoryStream や FileStream など)は、
Image / Bitmap のインスタンスが生きている間は閉じてはいけません。
元画像を閉じる処理は Image の Dispose に任せるようにしてください。

元画像は、Image / Bitmap のインスタンスを生成するタイミングで参照されるのはもちろん、
画像形式によっては、そのあとのタイミングでも画像情報が参照されることがあるためです。
投稿者 allgreen  (社会人) 投稿日時 2020/6/4 16:04:00
毎回、早い対応ありがとうございます
確かに、ストリーム.close をしなければエラーは出なくなりました

ただ、画像を読み込む、あとで書き込む ということをするために
ストリーム.close をしないと書き込めませんので
closeをすることは必要なのです

いろいろとやってみましたが、縦長の画像ではエラーにならずに横長の画像ではエラーに
なるのは、理由がわからないところです

あと、clone というのを検索しているところで、Deepcloneといものを見つけました
    Public Function DeepClone(Of T)(src As T) As T
        Using memoryStream = New System.IO.MemoryStream()
            Dim binaryFormatter = New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
            binaryFormatter.Serialize(memoryStream, src) ' シリアライズ
            memoryStream.Seek(0, System.IO.SeekOrigin.Begin)
            Return binaryFormatter.Deserialize(memoryStream) ' デシリアライズ
        End Using
    End Function
これを使えば、エラーにならず、ファイルのロックもされないことができて
解決できました
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/6/5 08:13:12
> 確かに、ストリーム.close をしなければエラーは出なくなりました

先に述べた理由があるため、元のストリームを閉じてはいけません。

特に、アニメーション GIF やマルチページ TIFF の場合、元ストリームが閉じていると、
PictureBox に表示させたタイミングや、SelectActiveFrame の呼び出しなどによって
エラーが引き起こされることがあります。


> ただ、画像を読み込む、あとで書き込む ということをするために
> ストリーム.close をしないと書き込めませんので

閉じるべきは「ストリーム」であって、「ファイル」ではありませんよ。
FileStream のかわりに MemoryStream を使うようにしてみてください。
たとえば、元ファイルを Byte 配列として読み込まれてすぐに閉じておき、
それを MemoryStream に引き渡すようにします。

Dim ms As New MemoryStream(File.ReadAllBytes(ファイルパス))

これならば、元のファイルを長期間ロックすることなく、Image.FromStream 用のストリームを調達することが出来るかと思います。

ただしこの方法をとる場合も、Image.FromStream した後は、Image が解放されるまで
ms.Dispose() しないようにします。(Image が Dispose されれば、元リソースも処分されるので
どちらにしてもストリームを明示的に処分する必要はありません)
投稿者 allgreen  (社会人) 投稿日時 2020/6/5 08:28:53
closeしないというのはわかるのですが、それでも不思議です

マルチ画像 = System.Drawing.Image.FromStream(ストリーム)
For i = 0 To ページ数 - 1
     マルチ画像.SelectActiveFrame(fd, i)
     分割画面(i) = マルチ画像.Clone
Next
ストリーム.Close()              これはエラーになる

マルチ画像 = System.Drawing.Image.FromStream(ストリーム)
For i = 0 To ページ数 - 1
     マルチ画像.SelectActiveFrame(fd, i)
     分割画面(i) = マルチ画像
Next
ストリーム.Close()              エラーにならない

マルチ画像 = System.Drawing.Image.FromStream(ストリーム)
For i = 0 To ページ数 - 1
     マルチ画像.SelectActiveFrame(fd, i)
     分割画面(i) = マルチ画像.Clone
Next                      エラーにならない

クローンでないときに、クローズするとエラーになるならわかるのですが
結果は逆なのです
クローンは、独立したものを作ると考えているのですが、不思議でわかりません
今回は、DeepCloneで解決できたからよいのですが
まだまだ、わからないことだらけです

投稿者 (削除されました)  () 投稿日時 2020/6/5 11:34:06
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/6/5 14:41:42
> クローンでないときに、クローズするとエラーになるならわかるのですが
> 結果は逆なのです
今回のケースでは、Clone した Image もまた、同じストリームへの参照を
有していたのだと考えてみると、わかりやすいかと思います。


たとえば下記の場合、Clone した bmp2 側を操作していますが、
fs.Dispose() していなければ、第5フレームに切り替わるのに対し、
fs.Dispose() していると、SelectActiveFrame の呼び出しで
ExternalException (GDI+ で汎用エラーが発生しました。) が飛んできます。


'複数解像度のマルチページ TIFF (1,971,467 バイト) 
' http://merovingio.c2rmf.cnrs.fr/iipimage/PalaisDuLouvre.tif 
Dim imgFile As String = "C:\temp\PalaisDuLouvre.tif"

Dim fs As New FileStream(imgFile, FileMode.Open)
Dim bmp1 As New Bitmap(fs)

'フレームを第3ページに切り替える 
bmp1.SelectActiveFrame(FrameDimension.Page, 3)

'クローンして bmp2 を生成 
Dim bmp2 As Bitmap = DirectCast(bmp1.Clone(), Bitmap)

fs.Dispose()
bmp2.SelectActiveFrame(FrameDimension.Page, 5)




> 分割画面(i) = マルチ画像.Clone
SelectActiveFrame メソッドを使っていますので、
「分割画面」というよりは「選択画面」かも知れません。

マルチ画像.GetFrameCount(FrameDimension.Page) > 1 だった場合、
分割画面(i).GetFrameCount(FrameDimension.Page) > 1 のままになるので、
これを『分割』と呼ぶのは、少し違和感がありました。

それぞれが単一フレームとなるよう写し取れば、「分割」っぽくなるかもしれませんね。
下記のコードだと、色数等が変化してしまいますが…。

Dim images As New List(Of Image)()
For idx As Integer = 0 To frameCount - 1
    srcImage.SelectActiveFrame(fd, idx)
    Dim bmp As New Bitmap(srcImage.Width, srcImage.Height)
    Using g = Graphics.FromImage(bmp)
        g.DrawImage(srcImage, 0, 0)
    End Using
    images.Add(bmp)
Next
Return images.ToArray()



> closeしないというのはわかるのですが、それでも不思議です
では、提示頂いた実験コードを順にみてみましょうか。
それぞれ上から A, B, C パターンと呼ぶことにします。
元画像となる「マルチ画像」は、3 つのページを持つ画像だとします。


まず最初は、エラーにならなかっという B のコードから見てみます。

> マルチ画像 = System.Drawing.Image.FromStream(ストリーム)
> For i = 0 To ページ数 - 1
>   マルチ画像.SelectActiveFrame(fd, i)
>   分割画面(i) = マルチ画像
> Next
この場合、
 マルチ画像 Is 分割画面(0) は True
 マルチ画像 Is 分割画面(1) は True
 マルチ画像 Is 分割画面(2) は True
の状態となります。全部同じインスタンスですね。

> ストリーム.Close()         エラーにならない
全ページを列挙選択したため、イメージ情報の一部がキャッシュされたことで、
偶然エラーにならなかったということではないでしょうか。
リソースからの追加読み込みが必要になった時点で、
やはりエラーになってしまう可能性があります。


続いて、A および C のコードです。
上記と同じように、まずはループ部分までを見てみます。

> マルチ画像 = System.Drawing.Image.FromStream(ストリーム)
> For i = 0 To ページ数 - 1
>   マルチ画像.SelectActiveFrame(fd, i)
>   分割画面(i) = マルチ画像.Clone
> Next
このループが終了した時点で、
 マルチ画像:「選択ページ番号:2」「参照リソース:開いているストリーム」
 分割画面(0):「選択ページ番号:0」「参照リソース:開いているストリーム」
 分割画面(1):「選択ページ番号:1」「参照リソース:開いているストリーム」
 分割画面(2):「選択ページ番号:2」「参照リソース:開いているストリーム」
の状態です。

この時点までには、Dispose も Close も行われていないので、何の問題もありません。

コード C では上記で終わりですが、コード A ではさらに下記のコードが実行されています。
> ストリーム.Close()    'これはエラーになる
これにより、
 マルチ画像:「選択ページ番号:2」「参照リソース:破棄されたストリーム」
 分割画面(0):「選択ページ番号:0」「参照リソース:破棄されたストリーム」
 分割画面(1):「選択ページ番号:1」「参照リソース:破棄されたストリーム」
 分割画面(2):「選択ページ番号:2」「参照リソース:破棄されたストリーム」
となります。
そのため、未キャッシュな情報を参照リソースからロードしようとしたタイミングで、
内部エラーを誘発する可能性があるかと思います。