diff --git a/Knossos.NET/Classes/APNGHelper.cs b/Knossos.NET/Classes/APNGHelper.cs new file mode 100644 index 00000000..ebdf43b1 --- /dev/null +++ b/Knossos.NET/Classes/APNGHelper.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Text; + +namespace Knossos.NET.Classes +{ + /// + /// Helper class to read APNG files. + /// + public static class APNGHelper + { + // PNG signature + private static readonly byte[] PngSignature = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + + /// + /// Reads a stream to verify if it is a valid APNG file + /// Checks for acTL chuck presence. + /// Doesn't close or disposes the stream. + /// Throws a exception if the stream doesn't contains a png file data + /// + /// + /// true if file is apng + /// + public static bool IsApng(Stream pngStream) + { + if (pngStream == null || !pngStream.CanRead) + throw new ArgumentException("Stream inválido."); + + long originalPos = pngStream.Position; + + try + { + using (var br = new BinaryReader(pngStream, Encoding.ASCII, leaveOpen: true)) + { + var sig = br.ReadBytes(8); + if (!BytesEqual(sig, PngSignature)) + throw new Exception("Incorrect PNG Signature"); + + // Look for acTL o IEND + while (pngStream.Position < pngStream.Length) + { + var lengthBytes = br.ReadBytes(4); + if (lengthBytes.Length < 4) break; + int length = ReadBigEndianInt(lengthBytes); + + var chunkType = Encoding.ASCII.GetString(br.ReadBytes(4)); + + if (chunkType == "acTL") + { + return true; + } + + pngStream.Seek(length + 4, SeekOrigin.Current); + + if (chunkType == "IEND") + break; + } + } + return false; + } + finally + { + pngStream.Seek(originalPos, SeekOrigin.Begin); + } + } + + + // Helpers + + private static bool BytesEqual(byte[] a, byte[] b) + { + if (a.Length != b.Length) return false; + for (int i = 0; i < a.Length; i++) + if (a[i] != b[i]) return false; + return true; + } + + private static int ReadBigEndianInt(byte[] bytes) + { + if (BitConverter.IsLittleEndian) Array.Reverse(bytes); + return BitConverter.ToInt32(bytes, 0); + } + } +} diff --git a/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs index ebce7ae0..44dbde34 100644 --- a/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs @@ -4,6 +4,7 @@ using Avalonia.Threading; using BBcodes; using CommunityToolkit.Mvvm.ComponentModel; +using Knossos.NET.Classes; using Knossos.NET.Models; using Knossos.NET.Views; using System; @@ -11,7 +12,6 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; @@ -86,6 +86,8 @@ public partial class ModDetailsViewModel : ViewModelBase [ObservableProperty] internal string? banner = null; [ObservableProperty] + internal string? apngBanner = null; + [ObservableProperty] internal bool forumAvailable = false; [ObservableProperty] internal bool isInstalled = false; @@ -281,9 +283,30 @@ private void LoadBanner(int selectedIndex) if (!string.IsNullOrEmpty(modVersions[selectedIndex].banner)) { HasBanner = true; - if (System.IO.File.Exists(modVersions[selectedIndex].fullPath + Path.DirectorySeparatorChar + modVersions[selectedIndex].banner)) + var bannerLocalPath = modVersions[selectedIndex].fullPath + Path.DirectorySeparatorChar + modVersions[selectedIndex].banner; + if (System.IO.File.Exists(bannerLocalPath)) { - Banner = modVersions[selectedIndex].fullPath + Path.DirectorySeparatorChar + modVersions[selectedIndex].banner; + var isApng = false; + using (var stream = new FileStream(bannerLocalPath, FileMode.Open, FileAccess.Read)) + { + try + { + isApng = APNGHelper.IsApng(stream); + + } + catch { /* Not a valid png*/ } + } + Dispatcher.UIThread.Invoke(() => + { + if (isApng) + { + ApngBanner = bannerLocalPath; + } + else + { + Banner = bannerLocalPath; + } + }); } else { @@ -293,9 +316,30 @@ private void LoadBanner(int selectedIndex) Task.Run(async () => { var fs = await KnUtils.GetRemoteResource(url).ConfigureAwait(false); - Dispatcher.UIThread.Invoke(() => { - Banner = fs; - }); + if (fs != null) + { + var isApng = false; + using (var stream = new FileStream(fs, FileMode.Open, FileAccess.Read)) + { + try + { + isApng = APNGHelper.IsApng(stream); + + } + catch { /* Not a valid png*/ } + } + Dispatcher.UIThread.Invoke(() => + { + if (isApng) + { + ApngBanner = fs; + } + else + { + Banner = fs; + } + }); + } }).ConfigureAwait(false); } } diff --git a/Knossos.NET/Views/Windows/ModDetailsView.axaml b/Knossos.NET/Views/Windows/ModDetailsView.axaml index d5ce51df..84a4e75c 100644 --- a/Knossos.NET/Views/Windows/ModDetailsView.axaml +++ b/Knossos.NET/Views/Windows/ModDetailsView.axaml @@ -27,7 +27,7 @@ - +