// OpenTween - Client of Twitter // Copyright (c) 2013 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. #nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; namespace OpenTween { /// /// Image と Stream を対に保持するためのクラス /// /// /// Image.FromStream() を使用して Image を生成する場合、 /// Image を破棄するまでの間は元となった Stream を破棄できないためその対策として使用する。 /// public class MemoryImage : ICloneable, IDisposable, IEquatable { private readonly byte[] buffer; private readonly int bufferOffset; private readonly int bufferCount; private readonly Image image; private static readonly Dictionary ExtensionByFormat = new() { { ImageFormat.Bmp, ".bmp" }, { ImageFormat.Emf, ".emf" }, { ImageFormat.Gif, ".gif" }, { ImageFormat.Icon, ".ico" }, { ImageFormat.Jpeg, ".jpg" }, { ImageFormat.MemoryBmp, ".bmp" }, { ImageFormat.Png, ".png" }, { ImageFormat.Tiff, ".tiff" }, { ImageFormat.Wmf, ".wmf" }, }; /// /// ストリームから読みだされる画像データが不正な場合にスローされる /// protected MemoryImage(byte[] buffer, int offset, int count) { try { this.buffer = buffer; this.bufferOffset = offset; this.bufferCount = count; this.Stream = new(buffer, offset, count, writable: false); this.image = this.CreateImage(this.Stream); } catch { this.Stream?.Dispose(); throw; } } private Image CreateImage(Stream stream) { try { return Image.FromStream(stream); } catch (ArgumentException e) { throw new InvalidImageException("Invalid image", e); } catch (OutOfMemoryException e) { // GDI+ がサポートしない画像形式で OutOfMemoryException がスローされる場合があるらしい throw new InvalidImageException("Invalid image?", e); } catch (ExternalException e) { // 「GDI+ で汎用エラーが発生しました」という大雑把な例外がスローされる場合があるらしい throw new InvalidImageException("Invalid image?", e); } } /// /// MemoryImage が保持している画像 /// public Image Image { get { if (this.IsDisposed) throw new ObjectDisposedException("this"); return this.image; } } /// /// MemoryImage が保持している画像のストリーム /// public MemoryStream Stream { get; } /// /// MemoryImage が破棄されているか否か /// public bool IsDisposed { get; private set; } = false; /// /// MemoryImage が保持している画像のフォーマット /// public ImageFormat ImageFormat => this.Image.RawFormat; /// /// MemoryImage が保持している画像のフォーマットに相当する拡張子 (ピリオド付き) /// public string ImageFormatExt => MemoryImage.ExtensionByFormat.TryGetValue(this.ImageFormat, out var ext) ? ext : ""; /// /// MemoryImage インスタンスを複製します /// /// 複製された MemoryImage public MemoryImage Clone() => new(this.buffer, this.bufferOffset, this.bufferCount); public override int GetHashCode() { using var sha1service = new System.Security.Cryptography.SHA1CryptoServiceProvider(); var hash = sha1service.ComputeHash(this.buffer, this.bufferOffset, this.bufferCount); return Convert.ToBase64String(hash).GetHashCode(); } public override bool Equals(object? other) => this.Equals(other as MemoryImage); public bool Equals(MemoryImage? other) { if (object.ReferenceEquals(this, other)) return true; if (other == null) return false; // それぞれが保持する MemoryStream の内容が等しいことを検証する var selfBuffer = new ArraySegment(this.buffer, this.bufferOffset, this.bufferCount); var otherBuffer = new ArraySegment(other.buffer, other.bufferOffset, other.bufferCount); if (selfBuffer.Count != otherBuffer.Count) return false; return selfBuffer.Zip(otherBuffer, (x, y) => x == y).All(x => x); } object ICloneable.Clone() => this.Clone(); protected virtual void Dispose(bool disposing) { if (this.IsDisposed) return; if (disposing) { this.Image.Dispose(); this.Stream.Dispose(); } this.IsDisposed = true; } public void Dispose() { this.Dispose(true); // 明示的にDisposeが呼ばれた場合はファイナライザを使用しない GC.SuppressFinalize(this); } ~MemoryImage() => this.Dispose(false); /// /// 指定された Stream から MemoryImage を作成します。 /// /// /// ストリームの内容はメモリ上に展開した後に使用されるため、 /// 引数に指定した Stream を MemoryImage より先に破棄しても問題ありません。 /// /// 読み込む対象となる Stream /// 作成された MemoryImage /// 不正な画像データが入力された場合 public static MemoryImage CopyFromStream(Stream stream) { using var memstream = new MemoryStream(); stream.CopyTo(memstream); return new(memstream.GetBuffer(), 0, (int)memstream.Length); } /// /// 指定された Stream から MemoryImage を非同期に作成します。 /// /// /// ストリームの内容はメモリ上に展開した後に使用されるため、 /// 引数に指定した Stream を MemoryImage より先に破棄しても問題ありません。 /// /// 読み込む対象となる Stream /// 作成された MemoryImage を返すタスク /// 不正な画像データが入力された場合 public static async Task CopyFromStreamAsync(Stream stream, int capacity = 0) { using var memstream = new MemoryStream(capacity); await stream.CopyToAsync(memstream) .ConfigureAwait(false); return new(memstream.GetBuffer(), 0, (int)memstream.Length); } /// /// 指定されたバイト列から MemoryImage を作成します。 /// /// 読み込む対象となるバイト列 /// 作成された MemoryImage /// 不正な画像データが入力された場合 public static MemoryImage CopyFromBytes(byte[] bytes) => new(bytes, 0, bytes.Length); /// /// Image インスタンスから MemoryImage を作成します /// /// /// PNG 画像として描画し直す処理を含むため、極力 Stream や byte[] を渡す他のメソッドを使用すべきです /// /// 対象となる画像 /// 作成された MemoryImage public static MemoryImage CopyFromImage(Image image) { using var memstream = new MemoryStream(); image.Save(memstream, ImageFormat.Png); return new(memstream.GetBuffer(), 0, (int)memstream.Length); } } /// /// 不正な画像データに対してスローされる例外 /// [Serializable] public class InvalidImageException : Exception { public InvalidImageException() { } public InvalidImageException(string message) : base(message) { } public InvalidImageException(string message, Exception innerException) : base(message, innerException) { } protected InvalidImageException(SerializationInfo info, StreamingContext context) : base(info, context) { } } }