// 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)
{
}
}
}