From bd72b109457e8a9553444714973e00865277c9e9 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Fri, 31 May 2024 05:04:53 +0900 Subject: [PATCH] =?UTF-8?q?WebP=E7=94=BB=E5=83=8F=E3=81=AE=E3=83=87?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.txt | 3 + OpenTween.Tests/OpenTween.Tests.csproj | 1 + OpenTween.Tests/Resources/re1.webp | Bin 0 -> 1380 bytes OpenTween.Tests/WebpDecoderTest.cs | 66 +++++++++++++++++++ OpenTween/MemoryImage.cs | 6 ++ OpenTween/WebpDecoder.cs | 86 +++++++++++++++++++++++++ 6 files changed, 162 insertions(+) create mode 100644 OpenTween.Tests/Resources/re1.webp create mode 100644 OpenTween.Tests/WebpDecoderTest.cs create mode 100644 OpenTween/WebpDecoder.cs 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 0000000000000000000000000000000000000000..5155b523e57ba550e3452b39a046a5a04d7d4f76 GIT binary patch literal 1380 zcmV-q1)KU(Nk&Fo1pok7MM6+kP&il$0000G0000#002J#06|PpNE-nF00DnM5D2ot zA?(jNL_}m5)B{qrZOft;cZom*8bAVR085t?Y@q=WU^Olgh%mZy_Zj^C-kX^K5&c)- zwvCcw=I!1fs>$~Dua5SsJ~5ii)^yD@ye3NDQ^iaI2D-mf)!c=gP-U=l+ro`?ksV7SAmC|{q^uH*_f6p)Fj`RPk@N?nk%I^ig zSNvY`d(GbkepefKmvYKTa=C!z3YSY*u0^?A#B!C(Wh~dZUch<<>!l#pYg8{Dd$3-G zdif-T_4)??1@3>vq3d5E09H^qAQ}V!05B8)odGI906+jfkwThDrKBPuDi&%muo4Mr z02X{cK>p3z2l$?d?+l->`YsT5sohS>{6qI2aUW0v#M}LE@LqO)mi7bvV*i`@kKO~& zWA!V`A3yyjr%sJhqdrIStu@!+&@Z^5!BElcYursa4dwo{ zb5H4gaf`<2D_FM6@8%A(tf4ovj+>6L*imv70g*Q|_%$^cvm8Pkqe%ZudQm9mDD|qX zyEr>?X`tO`Li5L&I0vu2qu)ulk~gu9E}wXA_aiqkJ$D{iu6%RqZAQ~>glIHOESpDg z=}_*i2I1s{Dp0^)kMt}%)Ak&Mz!=(cnD;6JU)rScuy-4*@^CT+p`0fqY%wZMD46q1 zPbhGXIX^42k=L21M4>Sdm&BcP9j;_}|6`R!Xl+}Jajkr+^#S>It*S@>i`>NOqmS6tE zX->_hCF3l~c1YUDy(Anf-{RH7*$giQ6DWC6HnGi2SWH)}HYUVXD$9%p-80ot8bGx( zagFQVPw_L67w7(+Tk9J#H-l~i#zfR}i7>8=BRIQyV+WOyCdQ+&QKtPre~2V&W%47B z)9eTrXfqK*7G=62&V%h8{eXMoQ$=Lbjd#M|dyi0Cr3*WH@3SK7OfmkckL$lREv84nc$B)F^8N+6(ofOO zNrRVvyuZrQr(*i2^iTK%T9}Use~uRF0v8 zJ`~>(T={Qv2DaMbcg8Ms9^-J)#$Ad%R20!{kHVTgMfA^p4d8!k3Eux5Bz`hn6>6tB mkQ=o+@U_) +// 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); + } + } + } +}