Merge pull request #1531 from wabbajack-tools/use-textconv

Switch to using TextConv for image compression
This commit is contained in:
Timothy Baldridge 2021-07-11 14:32:05 -07:00 committed by GitHub
commit b1f62fb6d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 241 additions and 389 deletions

View File

@ -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.True(hash1.Similarity(hash2) >= 0.8811f);
}
[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.True(hash1.Similarity(hash2) >= 0.1948f);
}
[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.True(hash1.Similarity(hash2) >= 0.92f);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -1,6 +0,0 @@
using System;
namespace Wabbajack.ImageHashing
{
}

View File

@ -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,70 @@ 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,
LogError = 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,
LogError = 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)
};
}
}
}

View File

@ -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);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -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>

View File

@ -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;

View File

@ -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]