diff --git a/Wabbajack.ImageHashing.Test/ImageLoadingTests.cs b/Wabbajack.ImageHashing.Test/ImageLoadingTests.cs index 849f7b10..39ce4dd6 100644 --- a/Wabbajack.ImageHashing.Test/ImageLoadingTests.cs +++ b/Wabbajack.ImageHashing.Test/ImageLoadingTests.cs @@ -9,50 +9,59 @@ namespace Wabbajack.ImageHashing.Test [Fact] public async Task CanLoadAndCompareDDSImages() { - var file1 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); - var hash1 = file1.PerceptionHash(); + var hash1 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); + var state1 = await ImageState.GetState(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); + + Assert.Equal(512, state1.Width); + Assert.Equal(512, state1.Height); + Assert.Equal(DXGI_FORMAT.BC3_UNORM, state1.Format); + + var hash2 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); + + // From old embedded hashing method, we want to make sure the hashing algorithm hasn't changed so much that + // we've broken the old caches + var hash3 = PHash.FromBase64("cns+/2xel0ulcwCXeTlVW2x5aGtwaGl9glpthWZkb2ducnF0c2lvgQ=="); - var file2 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); - var hash2 = file2.PerceptionHash(); Assert.Equal(1, hash1.Similarity(hash2)); + Assert.True(hash1.Similarity(hash3) > 0.99f); } [Fact] public async Task CanLoadAndCompareResizedImage() { - var file1 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); - var hash1 = file1.PerceptionHash(); + var hash1 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); - var file2 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7.dds")); - var hash2 = file2.PerceptionHash(); + var hash2 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7.dds")); - Assert.Equal(0.956666886806488, hash1.Similarity(hash2)); + var state2 = await ImageState.GetState(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7.dds")); + + Assert.Equal(64, state2.Width); + Assert.Equal(64, state2.Height); + Assert.Equal(DXGI_FORMAT.BC7_UNORM_SRGB, state2.Format); + + Assert.Equal(0.8811911940574646, hash1.Similarity(hash2)); } [Fact] public async Task CanLoadAndCompareResizedVFlipImage() { - var file1 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); - var hash1 = file1.PerceptionHash(); + var hash1 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); - var file2 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7-vflip.dds")); - var hash2 = file2.PerceptionHash(); + var hash2 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7-vflip.dds")); - Assert.Equal(0.2465425431728363, hash1.Similarity(hash2)); + Assert.Equal(0.19484494626522064, hash1.Similarity(hash2)); } [Fact] public async Task CanLoadAndCompareRecompressedImage() { - var file1 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); - var hash1 = file1.PerceptionHash(); + var hash1 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds")); - var file2 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-recompressed.dds")); - var hash2 = file2.PerceptionHash(); + var hash2 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-recompressed.dds")); - Assert.Equal(0.9999724626541138, hash1.Similarity(hash2)); + Assert.Equal(0.9298737645149231, hash1.Similarity(hash2)); } } } diff --git a/Wabbajack.ImageHashing/DDSImage.cs b/Wabbajack.ImageHashing/DDSImage.cs deleted file mode 100644 index 18b45620..00000000 --- a/Wabbajack.ImageHashing/DDSImage.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using DirectXTexNet; -using Shipwreck.Phash; -using Shipwreck.Phash.Imaging; -using Wabbajack.Common; - -namespace Wabbajack.ImageHashing -{ - public class DDSImage : IDisposable - { - private static Lazy<DX11Device> DX11Device = new(() => new DX11Device()); - private DDSImage(ScratchImage img, TexMetadata metadata, Extension ext) - { - _image = img; - _metaData = metadata; - _extension = ext; - } - - private static Extension DDSExtension = new(".dds"); - private static Extension TGAExtension = new(".tga"); - private ScratchImage _image; - private TexMetadata _metaData; - - public static DDSImage FromFile(AbsolutePath file) - { - if (file.Extension != DDSExtension) - throw new Exception("File does not end in DDS"); - - var img = TexHelper.Instance.LoadFromDDSFile(file.ToString(), DDS_FLAGS.NONE); - - return new DDSImage(img, img.GetMetadata(), new Extension(".dds")); - } - - public static DDSImage FromDDSMemory(byte[] data) - { - unsafe - { - fixed (byte* ptr = data) - { - var img = TexHelper.Instance.LoadFromDDSMemory((IntPtr)ptr, data.Length, DDS_FLAGS.NONE); - return new DDSImage(img, img.GetMetadata(), new Extension(".dds")); - } - } - } - - public static DDSImage FromTGAMemory(byte[] data) - { - unsafe - { - fixed (byte* ptr = data) - { - var img = TexHelper.Instance.LoadFromTGAMemory((IntPtr)ptr, data.Length); - return new DDSImage(img, img.GetMetadata(), new Extension(".tga")); - } - } - } - - - public static async Task<DDSImage> FromStream(Stream stream, IPath arg1Name) - { - var data = await stream.ReadAllAsync(); - if (arg1Name.FileName.Extension == DDSExtension) - return FromDDSMemory(data); - if (arg1Name.FileName.Extension == TGAExtension) - return FromTGAMemory(data); - throw new NotImplementedException("Only DDS and TGA files supported"); - } - - public void Dispose() - { - if (!_image.IsDisposed) - _image.Dispose(); - } - - public int Width => _metaData.Width; - public int Height => _metaData.Height; - - // Destructively resize a Image - public void ResizeRecompressAndSave(int width, int height, DXGI_FORMAT newFormat, AbsolutePath dest) - { - ScratchImage? resized = default; - try - { - // First we resize the image, so that changes due to image scaling matter less in the final hash - if (CompressedTypes.Contains(_metaData.Format)) - { - using var decompressed = _image.Decompress(DXGI_FORMAT.UNKNOWN); - resized = decompressed.Resize(width, height, TEX_FILTER_FLAGS.DEFAULT); - } - else - { - resized = _image.Resize(width, height, TEX_FILTER_FLAGS.DEFAULT); - } - - - if (CompressedTypes.Contains(newFormat)) - { - var old = resized; - resized = DX11Device.Value.Compress(resized, newFormat, TEX_COMPRESS_FLAGS.BC7_QUICK, 0.5f); - old.Dispose(); - } - - if (dest.Extension == new Extension(".dds")) - { - resized.SaveToDDSFile(DDS_FLAGS.NONE, dest.ToString()); - } - } - finally - { - resized?.Dispose(); - } - } - - private static HashSet<DXGI_FORMAT> CompressedTypes = new HashSet<DXGI_FORMAT>() - { - DXGI_FORMAT.BC1_TYPELESS, - DXGI_FORMAT.BC1_UNORM, - DXGI_FORMAT.BC1_UNORM_SRGB, - DXGI_FORMAT.BC2_TYPELESS, - DXGI_FORMAT.BC2_UNORM, - DXGI_FORMAT.BC2_UNORM_SRGB, - DXGI_FORMAT.BC3_TYPELESS, - DXGI_FORMAT.BC3_UNORM, - DXGI_FORMAT.BC3_UNORM_SRGB, - DXGI_FORMAT.BC4_TYPELESS, - DXGI_FORMAT.BC4_UNORM, - DXGI_FORMAT.BC4_SNORM, - DXGI_FORMAT.BC5_TYPELESS, - DXGI_FORMAT.BC5_UNORM, - DXGI_FORMAT.BC5_SNORM, - DXGI_FORMAT.BC6H_TYPELESS, - DXGI_FORMAT.BC6H_UF16, - DXGI_FORMAT.BC6H_SF16, - DXGI_FORMAT.BC7_TYPELESS, - DXGI_FORMAT.BC7_UNORM, - DXGI_FORMAT.BC7_UNORM_SRGB, - }; - - private Extension _extension; - - public ImageState ImageState() - { - return new() - { - Width = _metaData.Width, - Height = _metaData.Height, - Format = _metaData.Format, - PerceptualHash = PerceptionHash() - }; - } - - public PHash PerceptionHash() - { - ScratchImage? resized = default; - try - { - // First we resize the image, so that changes due to image scaling matter less in the final hash - if (CompressedTypes.Contains(_metaData.Format)) - { - using var decompressed = _image.Decompress(DXGI_FORMAT.UNKNOWN); - resized = decompressed.Resize(512, 512, TEX_FILTER_FLAGS.DEFAULT); - } - else - { - resized = _image.Resize(512, 512, TEX_FILTER_FLAGS.DEFAULT); - } - - var image = new byte[512 * 512]; - - unsafe void EvaluatePixels(IntPtr pixels, IntPtr width, IntPtr line) - { - float* ptr = (float*)pixels.ToPointer(); - - int widthV = width.ToInt32(); - if (widthV != 512) return; - - var y = line.ToInt32(); - - for (int i = 0; i < widthV; i++) - { - var r = ptr[0] * 0.229f; - var g = ptr[1] * 0.587f; - var b = ptr[2] * 0.114f; - - var combined = (r + g + b) * 255.0f; - - image[(y * widthV) + i] = (byte)combined; - ptr += 4; - } - - } - - resized.EvaluateImage(EvaluatePixels); - - var digest = ImagePhash.ComputeDigest(new ByteImage(512, 512, image)); - return PHash.FromDigest(digest); - } - finally - { - resized?.Dispose(); - } - } - - public void ResizeRecompressAndSave(ImageState state, AbsolutePath dest) - { - ResizeRecompressAndSave(state.Width, state.Height, state.Format, dest); - } - } -} diff --git a/Wabbajack.ImageHashing/DX11Device.cs b/Wabbajack.ImageHashing/DX11Device.cs deleted file mode 100644 index c27af5ae..00000000 --- a/Wabbajack.ImageHashing/DX11Device.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using DirectXTexNet; -using Silk.NET.Core.Native; -using Silk.NET.Direct3D11; - -namespace Wabbajack.ImageHashing -{ - public unsafe class DX11Device - { - private ID3D11Device* _device; - - public DX11Device() - { - unsafe - { - var dxgi = Silk.NET.DXGI.DXGI.GetApi(); - var dx11 = Silk.NET.Direct3D11.D3D11.GetApi(); - - D3DFeatureLevel[] levels = - { - //D3DFeatureLevel.D3DFeatureLevel100, D3DFeatureLevel.D3DFeatureLevel101, - D3DFeatureLevel.D3DFeatureLevel110 - }; - uint createDeviceFlags = 0; - - var adapterIdx = 0; - - D3DFeatureLevel fl; - ID3D11Device* device; - fixed (D3DFeatureLevel* lvls = levels) - { - var hr = dx11.CreateDevice(null, D3DDriverType.D3DDriverTypeHardware, IntPtr.Zero, - createDeviceFlags, lvls, - (uint)levels.Length, - Silk.NET.Direct3D11.D3D11.SdkVersion, &device, &fl, null); - - if (FAILED(hr)) - { - _device = null; - return; - } - - _device = device; - } - } - } - - public ScratchImage Compress(ScratchImage input, DXGI_FORMAT format, TEX_COMPRESS_FLAGS compress, - float threshold) - { - lock (this) - { - if (_device != null) - { - try - { - return input.Compress((IntPtr)_device, format, compress, threshold); - } - catch (COMException _) - { - _device->Release(); - _device = null; - } - } - return input.Compress(format, compress, threshold); - } - } - - private static bool FAILED(int x) - { - return x != 0; - } - } -} diff --git a/Wabbajack.ImageHashing/DXGI_FORMAT.cs b/Wabbajack.ImageHashing/DXGI_FORMAT.cs new file mode 100644 index 00000000..d3d8b0ac --- /dev/null +++ b/Wabbajack.ImageHashing/DXGI_FORMAT.cs @@ -0,0 +1,125 @@ +namespace Wabbajack.ImageHashing +{ + public enum DXGI_FORMAT + { + UNKNOWN = 0, + R32G32B32A32_TYPELESS = 1, + R32G32B32A32_FLOAT = 2, + R32G32B32A32_UINT = 3, + R32G32B32A32_SINT = 4, + R32G32B32_TYPELESS = 5, + R32G32B32_FLOAT = 6, + R32G32B32_UINT = 7, + R32G32B32_SINT = 8, + R16G16B16A16_TYPELESS = 9, + R16G16B16A16_FLOAT = 10, // 0x0000000A + R16G16B16A16_UNORM = 11, // 0x0000000B + R16G16B16A16_UINT = 12, // 0x0000000C + R16G16B16A16_SNORM = 13, // 0x0000000D + R16G16B16A16_SINT = 14, // 0x0000000E + R32G32_TYPELESS = 15, // 0x0000000F + R32G32_FLOAT = 16, // 0x00000010 + R32G32_UINT = 17, // 0x00000011 + R32G32_SINT = 18, // 0x00000012 + R32G8X24_TYPELESS = 19, // 0x00000013 + D32_FLOAT_S8X24_UINT = 20, // 0x00000014 + R32_FLOAT_X8X24_TYPELESS = 21, // 0x00000015 + X32_TYPELESS_G8X24_UINT = 22, // 0x00000016 + R10G10B10A2_TYPELESS = 23, // 0x00000017 + R10G10B10A2_UNORM = 24, // 0x00000018 + R10G10B10A2_UINT = 25, // 0x00000019 + R11G11B10_FLOAT = 26, // 0x0000001A + R8G8B8A8_TYPELESS = 27, // 0x0000001B + R8G8B8A8_UNORM = 28, // 0x0000001C + R8G8B8A8_UNORM_SRGB = 29, // 0x0000001D + R8G8B8A8_UINT = 30, // 0x0000001E + R8G8B8A8_SNORM = 31, // 0x0000001F + R8G8B8A8_SINT = 32, // 0x00000020 + R16G16_TYPELESS = 33, // 0x00000021 + R16G16_FLOAT = 34, // 0x00000022 + R16G16_UNORM = 35, // 0x00000023 + R16G16_UINT = 36, // 0x00000024 + R16G16_SNORM = 37, // 0x00000025 + R16G16_SINT = 38, // 0x00000026 + R32_TYPELESS = 39, // 0x00000027 + D32_FLOAT = 40, // 0x00000028 + R32_FLOAT = 41, // 0x00000029 + R32_UINT = 42, // 0x0000002A + R32_SINT = 43, // 0x0000002B + R24G8_TYPELESS = 44, // 0x0000002C + D24_UNORM_S8_UINT = 45, // 0x0000002D + R24_UNORM_X8_TYPELESS = 46, // 0x0000002E + X24_TYPELESS_G8_UINT = 47, // 0x0000002F + R8G8_TYPELESS = 48, // 0x00000030 + R8G8_UNORM = 49, // 0x00000031 + R8G8_UINT = 50, // 0x00000032 + R8G8_SNORM = 51, // 0x00000033 + R8G8_SINT = 52, // 0x00000034 + R16_TYPELESS = 53, // 0x00000035 + R16_FLOAT = 54, // 0x00000036 + D16_UNORM = 55, // 0x00000037 + R16_UNORM = 56, // 0x00000038 + R16_UINT = 57, // 0x00000039 + R16_SNORM = 58, // 0x0000003A + R16_SINT = 59, // 0x0000003B + R8_TYPELESS = 60, // 0x0000003C + R8_UNORM = 61, // 0x0000003D + R8_UINT = 62, // 0x0000003E + R8_SNORM = 63, // 0x0000003F + R8_SINT = 64, // 0x00000040 + A8_UNORM = 65, // 0x00000041 + R1_UNORM = 66, // 0x00000042 + R9G9B9E5_SHAREDEXP = 67, // 0x00000043 + R8G8_B8G8_UNORM = 68, // 0x00000044 + G8R8_G8B8_UNORM = 69, // 0x00000045 + BC1_TYPELESS = 70, // 0x00000046 + BC1_UNORM = 71, // 0x00000047 + BC1_UNORM_SRGB = 72, // 0x00000048 + BC2_TYPELESS = 73, // 0x00000049 + BC2_UNORM = 74, // 0x0000004A + BC2_UNORM_SRGB = 75, // 0x0000004B + BC3_TYPELESS = 76, // 0x0000004C + BC3_UNORM = 77, // 0x0000004D + BC3_UNORM_SRGB = 78, // 0x0000004E + BC4_TYPELESS = 79, // 0x0000004F + BC4_UNORM = 80, // 0x00000050 + BC4_SNORM = 81, // 0x00000051 + BC5_TYPELESS = 82, // 0x00000052 + BC5_UNORM = 83, // 0x00000053 + BC5_SNORM = 84, // 0x00000054 + B5G6R5_UNORM = 85, // 0x00000055 + B5G5R5A1_UNORM = 86, // 0x00000056 + B8G8R8A8_UNORM = 87, // 0x00000057 + B8G8R8X8_UNORM = 88, // 0x00000058 + R10G10B10_XR_BIAS_A2_UNORM = 89, // 0x00000059 + B8G8R8A8_TYPELESS = 90, // 0x0000005A + B8G8R8A8_UNORM_SRGB = 91, // 0x0000005B + B8G8R8X8_TYPELESS = 92, // 0x0000005C + B8G8R8X8_UNORM_SRGB = 93, // 0x0000005D + BC6H_TYPELESS = 94, // 0x0000005E + BC6H_UF16 = 95, // 0x0000005F + BC6H_SF16 = 96, // 0x00000060 + BC7_TYPELESS = 97, // 0x00000061 + BC7_UNORM = 98, // 0x00000062 + BC7_UNORM_SRGB = 99, // 0x00000063 + AYUV = 100, // 0x00000064 + Y410 = 101, // 0x00000065 + Y416 = 102, // 0x00000066 + NV12 = 103, // 0x00000067 + P010 = 104, // 0x00000068 + P016 = 105, // 0x00000069 + OPAQUE_420 = 106, // 0x0000006A + YUY2 = 107, // 0x0000006B + Y210 = 108, // 0x0000006C + Y216 = 109, // 0x0000006D + NV11 = 110, // 0x0000006E + AI44 = 111, // 0x0000006F + IA44 = 112, // 0x00000070 + P8 = 113, // 0x00000071 + A8P8 = 114, // 0x00000072 + B4G4R4A4_UNORM = 115, // 0x00000073 + P208 = 130, // 0x00000082 + V208 = 131, // 0x00000083 + V408 = 132, // 0x00000084 + } +} diff --git a/Wabbajack.ImageHashing/IImage.cs b/Wabbajack.ImageHashing/IImage.cs deleted file mode 100644 index 6973124a..00000000 --- a/Wabbajack.ImageHashing/IImage.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace Wabbajack.ImageHashing -{ - -} diff --git a/Wabbajack.ImageHashing/ImageState.cs b/Wabbajack.ImageHashing/ImageState.cs index 6b31ec78..6d003a47 100644 --- a/Wabbajack.ImageHashing/ImageState.cs +++ b/Wabbajack.ImageHashing/ImageState.cs @@ -1,9 +1,15 @@ using System; +using System.Collections.Generic; +using System.Drawing; using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Threading.Tasks; -using DirectXTexNet; +using Shipwreck.Phash; using Wabbajack.Common; using Wabbajack.Common.Serialization.Json; +using Shipwreck.Phash.Bitmaps; namespace Wabbajack.ImageHashing { @@ -36,36 +42,68 @@ namespace Wabbajack.ImageHashing public static async Task<ImageState?> FromImageStream(Stream stream, Extension ext, bool takeStreamOwnership = true) { - var ms = new MemoryStream(); - await stream.CopyToAsync(ms); - if (takeStreamOwnership) await stream.DisposeAsync(); + await using var tf = new TempFile(ext); + await tf.Path.WriteAllAsync(stream, takeStreamOwnership); + return await GetState(tf.Path); + } - DDSImage? img = default; - try - { - if (ext == new Extension(".dds")) - img = DDSImage.FromDDSMemory(ms.GetBuffer()); - else if (ext == new Extension(".tga")) - { - img = DDSImage.FromTGAMemory(ms.GetBuffer()); - } - else - { - throw new NotImplementedException("Only DDS and TGA files supported by PHash"); - } + private static readonly Extension PNGExtension = new(".png"); + public static async Task<PHash> GetPHash(AbsolutePath path) + { + await using var tmp = await TempFolder.Create(); + await ConvertImage(path, tmp.Dir, 512, 512, DXGI_FORMAT.R8G8B8A8_UNORM, PNGExtension); + + using var img = (Bitmap)Image.FromFile(path.FileName.RelativeTo(tmp.Dir).ReplaceExtension(PNGExtension).ToString()); + return PHash.FromDigest(ImagePhash.ComputeDigest(img.ToLuminanceImage())); + } - return img.ImageState(); + public static async Task ConvertImage(AbsolutePath from, AbsolutePath toFolder, int w, int h, DXGI_FORMAT format, Extension fileFormat) + { + // User isn't renaming the file, so we don't have to create a temporary folder + var ph = new ProcessHelper + { + Path = @"Tools\texconv.exe".RelativeTo(AbsolutePath.EntryPoint), + Arguments = new object[] {from, "-ft", fileFormat.ToString()[1..], "-f", format, "-o", toFolder, "-w", w, "-h", h, "-if", "CUBIC", "-singleproc"}, + ThrowOnNonZeroExitCode = true + }; + await ph.Start(); - } - catch (Exception ex) + } + + public static async Task ConvertImage(Stream from, ImageState state, Extension ext, AbsolutePath to) + { + await using var tmpFile = await TempFolder.Create(); + var inFile = to.FileName.RelativeTo(tmpFile.Dir).WithExtension(ext); + await inFile.WriteAllAsync(from); + await ConvertImage(inFile, to.Parent, state.Width, state.Height, state.Format, ext); + } + + public static async Task<ImageState> GetState(AbsolutePath path) + { + var ph = new ProcessHelper { - Utils.Log($"Unable to read image state (this is fine)"); - return null; - } - finally + Path = @"Tools\texdiag.exe".RelativeTo(AbsolutePath.EntryPoint), Arguments = new object[] {"info", path, "-nologo"}, + ThrowOnNonZeroExitCode = true + }; + var lines = new List<string>(); + using var _ = ph.Output.Where(p => p.Type == ProcessHelper.StreamType.Output) + .Select(p => p.Line) + .Where(p => p.Contains(" = ")) + .Subscribe(l => lines.Add(l)); + await ph.Start(); + var data = lines.Select(l => { - img?.Dispose(); - } + var split = l.Split(" = "); + return (split[0].Trim(), split[1].Trim()); + }).ToDictionary(p => p.Item1, p => p.Item2); + + return new ImageState + { + Width = int.Parse(data["width"]), + Height = int.Parse(data["height"]), + Format = Enum.Parse<DXGI_FORMAT>(data["format"]), + PerceptualHash = await GetPHash(path) + }; } } } diff --git a/Wabbajack.ImageHashing/PHash.cs b/Wabbajack.ImageHashing/PHash.cs index a5f17c7b..f844a24d 100644 --- a/Wabbajack.ImageHashing/PHash.cs +++ b/Wabbajack.ImageHashing/PHash.cs @@ -83,42 +83,6 @@ namespace Wabbajack.ImageHashing h <<= 8; return (int)h; } - - public static async Task<PHash> FromStream(Stream stream, Extension ext, bool takeStreamOwnership = true) - { - try - { - var ms = new MemoryStream(); - await stream.CopyToAsync(ms); - if (takeStreamOwnership) await stream.DisposeAsync(); - - DDSImage img; - if (ext == new Extension(".dds")) - img = DDSImage.FromDDSMemory(ms.GetBuffer()); - else if (ext == new Extension(".tga")) - { - img = DDSImage.FromTGAMemory(ms.GetBuffer()); - } - else - { - throw new NotImplementedException("Only DDS and TGA files supported by PHash"); - } - - return img.PerceptionHash(); - } - catch (Exception ex) - { - Utils.Log($"Error getting PHASH {ex}"); - return default; - } - } - - public static async Task<PHash> FromFile(AbsolutePath path) - { - await using var s = await path.OpenRead(); - return await FromStream(s, path.Extension); - - } } diff --git a/Wabbajack.ImageHashing/Tools/texconv.exe b/Wabbajack.ImageHashing/Tools/texconv.exe new file mode 100644 index 00000000..9228e6a2 Binary files /dev/null and b/Wabbajack.ImageHashing/Tools/texconv.exe differ diff --git a/Wabbajack.ImageHashing/Tools/texdiag.exe b/Wabbajack.ImageHashing/Tools/texdiag.exe new file mode 100644 index 00000000..f443a5aa Binary files /dev/null and b/Wabbajack.ImageHashing/Tools/texdiag.exe differ diff --git a/Wabbajack.ImageHashing/Wabbajack.ImageHashing.csproj b/Wabbajack.ImageHashing/Wabbajack.ImageHashing.csproj index c53d1461..82537375 100644 --- a/Wabbajack.ImageHashing/Wabbajack.ImageHashing.csproj +++ b/Wabbajack.ImageHashing/Wabbajack.ImageHashing.csproj @@ -9,12 +9,27 @@ <ItemGroup> <PackageReference Include="DirectXTexNet" Version="1.0.2" /> <PackageReference Include="Shipwreck.Phash" Version="0.5.0" /> + <PackageReference Include="Shipwreck.Phash.Bitmaps" Version="0.5.0" /> <PackageReference Include="Silk.NET.Direct3D11" Version="2.6.0" /> <PackageReference Include="Silk.NET.DXGI" Version="2.6.0" /> + <PackageReference Include="System.Drawing.Common" Version="5.0.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj" /> </ItemGroup> + <ItemGroup> + <Folder Include="Tools" /> + </ItemGroup> + + <ItemGroup> + <None Update="Tools\texconv.exe"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="Tools\texdiag.exe"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + </Project> diff --git a/Wabbajack.Lib/AInstaller.cs b/Wabbajack.Lib/AInstaller.cs index c6db2e01..424092eb 100644 --- a/Wabbajack.Lib/AInstaller.cs +++ b/Wabbajack.Lib/AInstaller.cs @@ -157,9 +157,7 @@ namespace Wabbajack.Lib case TransformedTexture tt: { await using var s = await sf.GetStream(); - using var img = await DDSImage.FromStream(s, vf.Name); - - img.ResizeRecompressAndSave(tt.ImageState, directive.Directive.To.RelativeTo(OutputFolder)); + await ImageState.ConvertImage(s, tt.ImageState, tt.To.Extension, directive.Directive.To.RelativeTo(OutputFolder)); } break; diff --git a/Wabbajack.Test/SanityTests.cs b/Wabbajack.Test/SanityTests.cs index af00249f..0f5945ff 100644 --- a/Wabbajack.Test/SanityTests.cs +++ b/Wabbajack.Test/SanityTests.cs @@ -8,13 +8,10 @@ using Wabbajack.Common; using Wabbajack.ImageHashing; using Wabbajack.Lib; using Wabbajack.Lib.CompilationSteps; -using Wabbajack.Lib.CompilationSteps.CompilationErrors; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; -using DXGI_FORMAT = DirectXTexNet.DXGI_FORMAT; -using File = Alphaleonis.Win32.Filesystem.File; -using Path = Alphaleonis.Win32.Filesystem.Path; +using DXGI_FORMAT = Wabbajack.ImageHashing.DXGI_FORMAT; namespace Wabbajack.Test { @@ -388,17 +385,17 @@ namespace Wabbajack.Test } { - using var originalDDS = DDSImage.FromFile(nativeFile); - originalDDS.ResizeRecompressAndSave(originalDDS.Width, originalDDS.Height, DXGI_FORMAT.BC7_UNORM, recompressedFile); - originalDDS.ResizeRecompressAndSave(128, 128, DXGI_FORMAT.BC7_UNORM, resizedFile); + var originalDDS = await ImageState.GetState(nativeFile); + await ImageState.ConvertImage(nativeFile, recompressedFile.Parent, originalDDS.Width, originalDDS.Height, DXGI_FORMAT.BC7_UNORM, recompressedFile.Extension); + await ImageState.ConvertImage(nativeFile, resizedFile.Parent, 128, 128, DXGI_FORMAT.BC7_UNORM, resizedFile.Extension); } await utils.Configure(); await CompileAndInstall(profile, true); await utils.VerifyInstalledFile(mod, @"native\whitestagbody.dds"); - Assert.True(0.99f <=(await PHash.FromFile(recompressedFile)).Similarity(await PHash.FromFile(utils.InstalledPath(mod, @"recompressed\whitestagbody.dds")))); - Assert.True(0.98f <=(await PHash.FromFile(resizedFile)).Similarity(await PHash.FromFile(utils.InstalledPath(mod, @"resized\whitestagbody.dds")))); + Assert.True(0.99f <=(await ImageState.GetState(recompressedFile)).PerceptualHash.Similarity(await ImageState.GetPHash(utils.InstalledPath(mod, @"recompressed\whitestagbody.dds")))); + Assert.True(0.98f <=(await ImageState.GetState(resizedFile)).PerceptualHash.Similarity(await ImageState.GetPHash(utils.InstalledPath(mod, @"resized\whitestagbody.dds")))); } [Fact]