diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0657dd64a..7641d57fb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -7,6 +7,9 @@ - 現時点ではメインアカウント以外のタブ設定は次回起動時に保持されません * NEW: Twemoji 15.1.0 に対応しました - Unicode 15.1 で追加された絵文字が表示されるようになります + * NEW: WebP画像の表示に対応しました + - プロフィール画像やサムネイル画像にWebPが使われている場合も表示が可能になります + - 「WebP画像拡張機能」がインストールされている環境でのみ動作します * CHG: 設定画面でのアカウント一覧の表示形式を変更 * CHG: 新規アカウント追加時のダイアログの構成を変更 * CHG: 新規タブの初回に読み込まれた発言を既読状態にする(起動時の初回の読み込みと同じ動作となる) diff --git a/OpenTween.Tests/OpenTween.Tests.csproj b/OpenTween.Tests/OpenTween.Tests.csproj index 6d5f37fd7..739464a9d 100644 --- a/OpenTween.Tests/OpenTween.Tests.csproj +++ b/OpenTween.Tests/OpenTween.Tests.csproj @@ -27,6 +27,7 @@ + all diff --git a/OpenTween.Tests/Resources/re1.webp b/OpenTween.Tests/Resources/re1.webp new file mode 100644 index 000000000..5155b523e Binary files /dev/null and b/OpenTween.Tests/Resources/re1.webp differ diff --git a/OpenTween.Tests/WebpDecoderTest.cs b/OpenTween.Tests/WebpDecoderTest.cs new file mode 100644 index 000000000..79f288788 --- /dev/null +++ b/OpenTween.Tests/WebpDecoderTest.cs @@ -0,0 +1,66 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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. + +using System.Drawing.Imaging; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class WebpDecoderTest + { + [Fact] + public async Task IsWebpImage_PNGTest() + { + using var imgStream = File.OpenRead("Resources/re1.png"); + using var memstream = new MemoryStream(); + await imgStream.CopyToAsync(memstream); + memstream.TryGetBuffer(out var buffer); + + Assert.False(WebpDecoder.IsWebpImage(buffer)); + } + + [Fact] + public async Task IsWebpImage_WebPTest() + { + using var imgStream = File.OpenRead("Resources/re1.webp"); + using var memstream = new MemoryStream(); + await imgStream.CopyToAsync(memstream); + memstream.TryGetBuffer(out var buffer); + + Assert.True(WebpDecoder.IsWebpImage(buffer)); + } + + [Fact] + public async Task ConvertFromWebp_Test() + { + using var imgStream = File.OpenRead("Resources/re1.webp"); + using var memstream = new MemoryStream(); + await imgStream.CopyToAsync(memstream); + memstream.TryGetBuffer(out var buffer); + + var converted = await WebpDecoder.ConvertFromWebp(buffer); + using var memoryImage = new MemoryImage(converted); + Assert.Equal(ImageFormat.Png, memoryImage.ImageFormat); + } + } +} diff --git a/OpenTween/MemoryImage.cs b/OpenTween/MemoryImage.cs index c45b4bfba..0e913ee8c 100644 --- a/OpenTween/MemoryImage.cs +++ b/OpenTween/MemoryImage.cs @@ -244,6 +244,12 @@ await stream.CopyToAsync(memstream) var ret = memstream.TryGetBuffer(out var buffer); Debug.Assert(ret, "TryGetBuffer() == true"); + if (WebpDecoder.IsWebpImage(buffer)) + { + var transcoded = await WebpDecoder.ConvertFromWebp(buffer); + return new(transcoded); + } + return new(buffer); } diff --git a/OpenTween/WebpDecoder.cs b/OpenTween/WebpDecoder.cs new file mode 100644 index 000000000..217887dbf --- /dev/null +++ b/OpenTween/WebpDecoder.cs @@ -0,0 +1,86 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Windows.Graphics.Imaging; + +namespace OpenTween +{ + public class WebpDecoder + { + // reference: https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern + private static readonly byte[] PatternMask = + { + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + }; + + private static readonly byte[] BytePattern = + { + 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, + 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, + }; + + public static bool IsWebpImage(ArraySegment buffer) + { + return buffer.WithIndex().Take(PatternMask.Length) + .All(x => (x.Value & PatternMask[x.Index]) == BytePattern[x.Index]); + } + + public static async Task> ConvertFromWebp(ArraySegment inputBuffer) + { + const uint WINCODEC_ERR_COMPONENTINITIALIZEFAILURE = 0x88982F8B; + + try + { + using var inputStream = new MemoryStream(inputBuffer.Array, inputBuffer.Offset, inputBuffer.Count, writable: false, publiclyVisible: true); + using var raInStream = inputStream.AsRandomAccessStream(); + var decoder = await BitmapDecoder.CreateAsync(raInStream); + using var softwareBitmap = await decoder.GetSoftwareBitmapAsync(); + + using var outputStream = new MemoryStream(); + using var raOutStream = outputStream.AsRandomAccessStream(); + + var encoderId = BitmapEncoder.PngEncoderId; + var encoder = await BitmapEncoder.CreateAsync(encoderId, raOutStream); + encoder.SetSoftwareBitmap(softwareBitmap); + await encoder.FlushAsync(); + + var ret = outputStream.TryGetBuffer(out var outBuffer); + Debug.Assert(ret, "TryGetBuffer() == true"); + + return outBuffer; + } + catch (Exception ex) + when (unchecked((uint)ex.HResult) == WINCODEC_ERR_COMPONENTINITIALIZEFAILURE) + { + // 「WebP画像拡張機能」がインストールされていない環境ではエラーになる + throw new InvalidImageException($"WebP codec initialization error (HRESULT: 0x{WINCODEC_ERR_COMPONENTINITIALIZEFAILURE:X})", ex); + } + } + } +}