OSDN Git Service

バージョン v2.4.2-dev 開発開始
[opentween/open-tween.git] / OpenTween / MemoryImage.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2013 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details.
16 //
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 #nullable enable
23
24 using System;
25 using System.Collections.Generic;
26 using System.Linq;
27 using System.Runtime.InteropServices;
28 using System.Runtime.Serialization;
29 using System.Text;
30 using System.Diagnostics.CodeAnalysis;
31 using System.Drawing;
32 using System.IO;
33 using System.Threading.Tasks;
34 using System.Drawing.Imaging;
35
36 namespace OpenTween
37 {
38     /// <summary>
39     /// Image と Stream を対に保持するためのクラス
40     /// </summary>
41     /// <remarks>
42     /// Image.FromStream() を使用して Image を生成する場合、
43     /// Image を破棄するまでの間は元となった Stream を破棄できないためその対策として使用する。
44     /// </remarks>
45     public class MemoryImage : ICloneable, IDisposable, IEquatable<MemoryImage>
46     {
47         private readonly Image image;
48
49         /// <exception cref="InvalidImageException">
50         /// ストリームから読みだされる画像データが不正な場合にスローされる
51         /// </exception>
52         protected MemoryImage(MemoryStream stream)
53         {
54             try
55             {
56                 this.image = Image.FromStream(stream);
57             }
58             catch (ArgumentException e)
59             {
60                 stream.Dispose();
61                 throw new InvalidImageException("Invalid image", e);
62             }
63             catch (OutOfMemoryException e)
64             {
65                 // GDI+ がサポートしない画像形式で OutOfMemoryException がスローされる場合があるらしい
66                 stream.Dispose();
67                 throw new InvalidImageException("Invalid image?", e);
68             }
69             catch (ExternalException e)
70             {
71                 // 「GDI+ で汎用エラーが発生しました」という大雑把な例外がスローされる場合があるらしい
72                 stream.Dispose();
73                 throw new InvalidImageException("Invalid image?", e);
74             }
75             catch (Exception)
76             {
77                 stream.Dispose();
78                 throw;
79             }
80
81             this.Stream = stream;
82         }
83
84         /// <summary>
85         /// MemoryImage が保持している画像
86         /// </summary>
87         public Image Image
88         {
89             get
90             {
91                 if (this.IsDisposed)
92                     throw new ObjectDisposedException("this");
93
94                 return this.image;
95             }
96         }
97
98         /// <summary>
99         /// MemoryImage が保持している画像のストリーム
100         /// </summary>
101         public MemoryStream Stream { get; }
102
103         /// <summary>
104         /// MemoryImage が破棄されているか否か
105         /// </summary>
106         public bool IsDisposed { get; private set; } = false;
107
108         /// <summary>
109         /// MemoryImage が保持している画像のフォーマット
110         /// </summary>
111         public ImageFormat ImageFormat
112             => this.Image.RawFormat;
113
114         /// <summary>
115         /// MemoryImage が保持している画像のフォーマットに相当する拡張子 (ピリオド付き)
116         /// </summary>
117         public string ImageFormatExt
118         {
119             get
120             {
121                 var format = this.ImageFormat;
122
123                 // ImageFormat は == で正しく比較できないため Equals を使用する必要がある
124                 if (format.Equals(ImageFormat.Bmp))
125                     return ".bmp";
126                 if (format.Equals(ImageFormat.Emf))
127                     return ".emf";
128                 if (format.Equals(ImageFormat.Gif))
129                     return ".gif";
130                 if (format.Equals(ImageFormat.Icon))
131                     return ".ico";
132                 if (format.Equals(ImageFormat.Jpeg))
133                     return ".jpg";
134                 if (format.Equals(ImageFormat.MemoryBmp))
135                     return ".bmp";
136                 if (format.Equals(ImageFormat.Png))
137                     return ".png";
138                 if (format.Equals(ImageFormat.Tiff))
139                     return ".tiff";
140                 if (format.Equals(ImageFormat.Wmf))
141                     return ".wmf";
142
143                 // 対応する形式がなければ空文字列を返す
144                 // (上記以外のフォーマットは Image.FromStream を通過できないため、ここが実行されることはまず無い)
145                 return string.Empty;
146             }
147         }
148
149         /// <summary>
150         /// MemoryImage インスタンスを複製します
151         /// </summary>
152         /// <remarks>
153         /// メソッド実行中にストリームのシークが行われないよう注意して下さい。
154         /// 特に PictureBox で Gif アニメーションを表示している場合は Enabled に false をセットするなどして更新を止めて下さい。
155         /// </remarks>
156         /// <returns>複製された MemoryImage</returns>
157         public MemoryImage Clone()
158         {
159             this.Stream.Seek(0, SeekOrigin.Begin);
160
161             return MemoryImage.CopyFromStream(this.Stream);
162         }
163
164         /// <summary>
165         /// MemoryImage インスタンスを非同期に複製します
166         /// </summary>
167         /// <remarks>
168         /// メソッド実行中にストリームのシークが行われないよう注意して下さい。
169         /// 特に PictureBox で Gif アニメーションを表示している場合は Enabled に false をセットするなどして更新を止めて下さい。
170         /// </remarks>
171         /// <returns>複製された MemoryImage を返すタスク</returns>
172         public Task<MemoryImage> CloneAsync()
173         {
174             this.Stream.Seek(0, SeekOrigin.Begin);
175
176             return MemoryImage.CopyFromStreamAsync(this.Stream);
177         }
178
179         public override int GetHashCode()
180         {
181             using var sha1service = new System.Security.Cryptography.SHA1CryptoServiceProvider();
182             var hash = sha1service.ComputeHash(this.Stream.GetBuffer(), 0, (int)this.Stream.Length);
183             return Convert.ToBase64String(hash).GetHashCode();
184         }
185
186         public override bool Equals(object? other)
187             => this.Equals(other as MemoryImage);
188
189         public bool Equals(MemoryImage? other)
190         {
191             if (object.ReferenceEquals(this, other))
192                 return true;
193
194             if (other == null)
195                 return false;
196
197             // それぞれが保持する MemoryStream の内容が等しいことを検証する
198
199             var selfLength = this.Stream.Length;
200             var otherLength = other.Stream.Length;
201
202             if (selfLength != otherLength)
203                 return false;
204
205             var selfBuffer = this.Stream.GetBuffer();
206             var otherBuffer = other.Stream.GetBuffer();
207
208             for (var pos = 0L; pos < selfLength; pos++)
209             {
210                 if (selfBuffer[pos] != otherBuffer[pos])
211                     return false;
212             }
213
214             return true;
215         }
216
217         object ICloneable.Clone()
218             => this.Clone();
219
220         protected virtual void Dispose(bool disposing)
221         {
222             if (this.IsDisposed) return;
223
224             if (disposing)
225             {
226                 this.Image.Dispose();
227                 this.Stream.Dispose();
228             }
229
230             this.IsDisposed = true;
231         }
232
233         public void Dispose()
234         {
235             this.Dispose(true);
236
237             // 明示的にDisposeが呼ばれた場合はファイナライザを使用しない
238             GC.SuppressFinalize(this);
239         }
240
241         ~MemoryImage()
242             => this.Dispose(false);
243
244         /// <summary>
245         /// 指定された Stream から MemoryImage を作成します。
246         /// </summary>
247         /// <remarks>
248         /// ストリームの内容はメモリ上に展開した後に使用されるため、
249         /// 引数に指定した Stream を MemoryImage より先に破棄しても問題ありません。
250         /// </remarks>
251         /// <param name="stream">読み込む対象となる Stream</param>
252         /// <returns>作成された MemoryImage</returns>
253         /// <exception cref="InvalidImageException">不正な画像データが入力された場合</exception>
254         public static MemoryImage CopyFromStream(Stream stream)
255         {
256             MemoryStream? memstream = null;
257             try
258             {
259                 memstream = new MemoryStream();
260
261                 stream.CopyTo(memstream);
262
263                 return new MemoryImage(memstream);
264             }
265             catch
266             {
267                 memstream?.Dispose();
268                 throw;
269             }
270         }
271
272         /// <summary>
273         /// 指定された Stream から MemoryImage を非同期に作成します。
274         /// </summary>
275         /// <remarks>
276         /// ストリームの内容はメモリ上に展開した後に使用されるため、
277         /// 引数に指定した Stream を MemoryImage より先に破棄しても問題ありません。
278         /// </remarks>
279         /// <param name="stream">読み込む対象となる Stream</param>
280         /// <returns>作成された MemoryImage を返すタスク</returns>
281         /// <exception cref="InvalidImageException">不正な画像データが入力された場合</exception>
282         public async static Task<MemoryImage> CopyFromStreamAsync(Stream stream)
283         {
284             MemoryStream? memstream = null;
285             try
286             {
287                 memstream = new MemoryStream();
288
289                 await stream.CopyToAsync(memstream).ConfigureAwait(false);
290
291                 return new MemoryImage(memstream);
292             }
293             catch
294             {
295                 memstream?.Dispose();
296                 throw;
297             }
298         }
299
300         /// <summary>
301         /// 指定されたバイト列から MemoryImage を作成します。
302         /// </summary>
303         /// <param name="bytes">読み込む対象となるバイト列</param>
304         /// <returns>作成された MemoryImage</returns>
305         /// <exception cref="InvalidImageException">不正な画像データが入力された場合</exception>
306         public static MemoryImage CopyFromBytes(byte[] bytes)
307         {
308             MemoryStream? memstream = null;
309             try
310             {
311                 memstream = new MemoryStream(bytes);
312                 return new MemoryImage(memstream);
313             }
314             catch
315             {
316                 memstream?.Dispose();
317                 throw;
318             }
319         }
320
321         /// <summary>
322         /// Image インスタンスから MemoryImage を作成します
323         /// </summary>
324         /// <remarks>
325         /// PNG 画像として描画し直す処理を含むため、極力 Stream や byte[] を渡す他のメソッドを使用すべきです
326         /// </remarks>
327         /// <param name="image">対象となる画像</param>
328         /// <returns>作成された MemoryImage</returns>
329         public static MemoryImage CopyFromImage(Image image)
330         {
331             MemoryStream? memstream = null;
332             try
333             {
334                 memstream = new MemoryStream();
335
336                 image.Save(memstream, ImageFormat.Png);
337
338                 return new MemoryImage(memstream);
339             }
340             catch
341             {
342                 memstream?.Dispose();
343                 throw;
344             }
345         }
346     }
347
348     /// <summary>
349     /// 不正な画像データに対してスローされる例外
350     /// </summary>
351     [Serializable]
352     public class InvalidImageException : Exception
353     {
354         public InvalidImageException() { }
355         public InvalidImageException(string message) : base(message) { }
356         public InvalidImageException(string message, Exception innerException) : base(message, innerException) { }
357         protected InvalidImageException(SerializationInfo info, StreamingContext context) : base(info, context) { }
358     }
359 }