Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebP画像のデコードに対応 #349

Merged
merged 3 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ env:
jobs:
build:
uses: ./.github/workflows/build.yml
with:
msbuild_args: /p:ContinuousIntegrationBuild=true

test:
runs-on: windows-2022
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- 現時点ではメインアカウント以外のタブ設定は次回起動時に保持されません
* NEW: Twemoji 15.1.0 に対応しました
- Unicode 15.1 で追加された絵文字が表示されるようになります
* NEW: WebP画像の表示に対応しました
- プロフィール画像やサムネイル画像にWebPが使われている場合も表示が可能になります
- 「WebP画像拡張機能」がインストールされている環境でのみ動作します
* CHG: 設定画面でのアカウント一覧の表示形式を変更
* CHG: 新規アカウント追加時のダイアログの構成を変更
* CHG: 新規タブの初回に読み込まれた発言を既読状態にする(起動時の初回の読み込みと同じ動作となる)
Expand Down
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<DefineConstants Condition="'$(ContinuousIntegrationBuild)' == 'true'">$(DefineConstants);CI_BUILD</DefineConstants>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions OpenTween.Tests/OpenTween.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="altcover" Version="8.6.95" />
<PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.19041.2" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507">
<PrivateAssets>all</PrivateAssets>
Expand Down
Binary file added OpenTween.Tests/Resources/re1.webp
Binary file not shown.
91 changes: 91 additions & 0 deletions OpenTween.Tests/WebpDecoderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// OpenTween - Client of Twitter
// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~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 <http://www.gnu.org/licenses/>, 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));
}

#if CI_BUILD
#pragma warning disable xUnit1004
[Fact(Skip = "WebP画像拡張機能がインストールされている環境でしか動作しない")]
#pragma warning restore xUnit1004
#else
[Fact]
#endif
public async Task ConvertFromWebp_SuccessTest()
{
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);
}

#if CI_BUILD
[Fact]
#else
#pragma warning disable xUnit1004
[Fact(Skip = "WebP画像拡張機能がインストールされていない環境に対するテスト")]
#pragma warning restore xUnit1004
#endif
public async Task ConvertFromWebp_FailTest()
{
using var imgStream = File.OpenRead("Resources/re1.webp");
using var memstream = new MemoryStream();
await imgStream.CopyToAsync(memstream);
memstream.TryGetBuffer(out var buffer);

await Assert.ThrowsAsync<InvalidImageException>(
() => WebpDecoder.ConvertFromWebp(buffer)
);
}
}
}
50 changes: 32 additions & 18 deletions OpenTween/MemoryImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Imaging;
Expand All @@ -44,9 +45,7 @@ namespace OpenTween
/// </remarks>
public class MemoryImage : ICloneable, IDisposable, IEquatable<MemoryImage>
{
private readonly byte[] buffer;
private readonly int bufferOffset;
private readonly int bufferCount;
private readonly ArraySegment<byte> buffer;
private readonly Image image;

private static readonly Dictionary<ImageFormat, string> ExtensionByFormat = new()
Expand All @@ -65,15 +64,13 @@ public class MemoryImage : ICloneable, IDisposable, IEquatable<MemoryImage>
/// <exception cref="InvalidImageException">
/// ストリームから読みだされる画像データが不正な場合にスローされる
/// </exception>
protected MemoryImage(byte[] buffer, int offset, int count)
public MemoryImage(ArraySegment<byte> buffer)
{
try
{
this.buffer = buffer;
this.bufferOffset = offset;
this.bufferCount = count;

this.Stream = new(buffer, offset, count, writable: false);
this.Stream = new(buffer.Array, buffer.Offset, buffer.Count, writable: false);
this.image = this.CreateImage(this.Stream);
}
catch
Expand Down Expand Up @@ -146,12 +143,17 @@ public string ImageFormatExt
/// </summary>
/// <returns>複製された MemoryImage</returns>
public MemoryImage Clone()
=> new(this.buffer, this.bufferOffset, this.bufferCount);
=> new(this.buffer);

public override int GetHashCode()
{
using var sha1service = new System.Security.Cryptography.SHA1CryptoServiceProvider();
var hash = sha1service.ComputeHash(this.buffer, this.bufferOffset, this.bufferCount);

var rawBuffer = this.buffer.Array;
var offset = this.buffer.Offset;
var count = this.buffer.Count;
var hash = sha1service.ComputeHash(rawBuffer, offset, count);

return Convert.ToBase64String(hash).GetHashCode();
}

Expand All @@ -167,13 +169,10 @@ public bool Equals(MemoryImage? other)
return false;

// それぞれが保持する MemoryStream の内容が等しいことを検証する
var selfBuffer = new ArraySegment<byte>(this.buffer, this.bufferOffset, this.bufferCount);
var otherBuffer = new ArraySegment<byte>(other.buffer, other.bufferOffset, other.bufferCount);

if (selfBuffer.Count != otherBuffer.Count)
if (this.buffer.Count != other.buffer.Count)
return false;

return selfBuffer.Zip(otherBuffer, (x, y) => x == y).All(x => x);
return this.buffer.Zip(other.buffer, (x, y) => x == y).All(x => x);
}

object ICloneable.Clone()
Expand Down Expand Up @@ -219,7 +218,10 @@ public static MemoryImage CopyFromStream(Stream stream)

stream.CopyTo(memstream);

return new(memstream.GetBuffer(), 0, (int)memstream.Length);
var ret = memstream.TryGetBuffer(out var buffer);
Debug.Assert(ret, "TryGetBuffer() == true");

return new(buffer);
}

/// <summary>
Expand All @@ -239,7 +241,16 @@ public static async Task<MemoryImage> CopyFromStreamAsync(Stream stream, int cap
await stream.CopyToAsync(memstream)
.ConfigureAwait(false);

return new(memstream.GetBuffer(), 0, (int)memstream.Length);
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);
}

/// <summary>
Expand All @@ -249,7 +260,7 @@ await stream.CopyToAsync(memstream)
/// <returns>作成された MemoryImage</returns>
/// <exception cref="InvalidImageException">不正な画像データが入力された場合</exception>
public static MemoryImage CopyFromBytes(byte[] bytes)
=> new(bytes, 0, bytes.Length);
=> new(new(bytes));

/// <summary>
/// Image インスタンスから MemoryImage を作成します
Expand All @@ -265,7 +276,10 @@ public static MemoryImage CopyFromImage(Image image)

image.Save(memstream, ImageFormat.Png);

return new(memstream.GetBuffer(), 0, (int)memstream.Length);
var ret = memstream.TryGetBuffer(out var buffer);
Debug.Assert(ret, "TryGetBuffer() == true");

return new(buffer);
}
}

Expand Down
86 changes: 86 additions & 0 deletions OpenTween/WebpDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// OpenTween - Client of Twitter
// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~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 <http://www.gnu.org/licenses/>, 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<byte> buffer)
{
return buffer.WithIndex().Take(PatternMask.Length)
.All(x => (x.Value & PatternMask[x.Index]) == BytePattern[x.Index]);
}

public static async Task<ArraySegment<byte>> ConvertFromWebp(ArraySegment<byte> 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);
}
}
}
}
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ init:

before_build:
- nuget restore
- ps: Set-Content .\msbuild.rsp "/warnaserror /p:DebugType=None"
- ps: Set-Content .\msbuild.rsp "/warnaserror /p:DebugType=None /p:ContinuousIntegrationBuild=true"

test:
assemblies:
Expand Down
Loading