2021-09-27 12:42:46 +00:00
|
|
|
using System;
|
2022-12-28 16:21:58 +00:00
|
|
|
using System.Collections.Generic;
|
2021-09-27 12:42:46 +00:00
|
|
|
using System.IO;
|
2023-01-21 19:36:12 +00:00
|
|
|
using System.Runtime.InteropServices;
|
2021-09-27 12:42:46 +00:00
|
|
|
using System.Threading;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using BCnEncoder.Decoder;
|
|
|
|
using BCnEncoder.Encoder;
|
|
|
|
using BCnEncoder.ImageSharp;
|
|
|
|
using BCnEncoder.Shared;
|
|
|
|
using BCnEncoder.Shared.ImageFiles;
|
|
|
|
using Shipwreck.Phash;
|
|
|
|
using Shipwreck.Phash.Imaging;
|
|
|
|
using SixLabors.ImageSharp;
|
|
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
|
|
using SixLabors.ImageSharp.Processing;
|
2023-01-21 19:36:12 +00:00
|
|
|
using Wabbajack.Common;
|
2021-09-27 12:42:46 +00:00
|
|
|
using Wabbajack.DTOs.Texture;
|
|
|
|
using Wabbajack.Paths;
|
|
|
|
using Wabbajack.Paths.IO;
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
namespace Wabbajack.Hashing.PHash;
|
|
|
|
|
2023-01-21 19:36:12 +00:00
|
|
|
public class CrossPlatformImageLoader : IImageLoader
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2023-01-21 19:36:12 +00:00
|
|
|
public async ValueTask<ImageState> Load(AbsolutePath path)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
|
|
return await Load(fs);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2023-01-21 19:36:12 +00:00
|
|
|
public async ValueTask<ImageState> Load(Stream stream)
|
2021-10-23 16:51:17 +00:00
|
|
|
{
|
|
|
|
var decoder = new BcDecoder();
|
|
|
|
var ddsFile = DdsFile.Load(stream);
|
|
|
|
var data = await decoder.DecodeToImageRgba32Async(ddsFile);
|
|
|
|
|
|
|
|
var format = ddsFile.dx10Header.dxgiFormat == DxgiFormat.DxgiFormatUnknown
|
|
|
|
? ddsFile.header.ddsPixelFormat.DxgiFormat
|
|
|
|
: ddsFile.dx10Header.dxgiFormat;
|
|
|
|
|
|
|
|
var state = new ImageState
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
Width = data.Width,
|
|
|
|
Height = data.Height,
|
|
|
|
Format = (DXGI_FORMAT) format
|
|
|
|
};
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
data.Mutate(x => x.Resize(512, 512, KnownResamplers.Welch).Grayscale(GrayscaleMode.Bt601));
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
var hash = ImagePhash.ComputeDigest(new ImageBitmap(data));
|
|
|
|
state.PerceptualHash = new DTOs.Texture.PHash(hash.Coefficients);
|
|
|
|
return state;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public static float ComputeDifference(DTOs.Texture.PHash a, DTOs.Texture.PHash b)
|
|
|
|
{
|
|
|
|
return ImagePhash.GetCrossCorrelation(
|
|
|
|
new Digest {Coefficients = a.Data},
|
|
|
|
new Digest {Coefficients = b.Data});
|
|
|
|
}
|
2023-01-21 19:36:12 +00:00
|
|
|
|
|
|
|
public async Task Recompress(AbsolutePath input, int width, int height, DXGI_FORMAT format,
|
2021-10-23 16:51:17 +00:00
|
|
|
AbsolutePath output,
|
|
|
|
CancellationToken token)
|
|
|
|
{
|
|
|
|
var inData = await input.ReadAllBytesAsync(token);
|
|
|
|
await using var outStream = output.Open(FileMode.Create, FileAccess.Write);
|
|
|
|
await Recompress(new MemoryStream(inData), width, height, format, outStream, token);
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2023-01-21 19:36:12 +00:00
|
|
|
public async Task Recompress(Stream input, int width, int height, DXGI_FORMAT format, Stream output,
|
2021-10-23 16:51:17 +00:00
|
|
|
CancellationToken token, bool leaveOpen = false)
|
|
|
|
{
|
|
|
|
var decoder = new BcDecoder();
|
|
|
|
var ddsFile = DdsFile.Load(input);
|
2023-01-21 19:36:12 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
if (!leaveOpen) await input.DisposeAsync();
|
|
|
|
|
2022-12-28 16:21:58 +00:00
|
|
|
var faces = new List<Image<Rgba32>>();
|
2023-01-21 19:36:12 +00:00
|
|
|
|
2022-12-28 16:21:58 +00:00
|
|
|
var origFormat = ddsFile.dx10Header.dxgiFormat == DxgiFormat.DxgiFormatUnknown
|
|
|
|
? ddsFile.header.ddsPixelFormat.DxgiFormat
|
|
|
|
: ddsFile.dx10Header.dxgiFormat;
|
|
|
|
|
|
|
|
foreach (var face in ddsFile.Faces)
|
|
|
|
{
|
2023-01-21 19:36:12 +00:00
|
|
|
var data = await decoder.DecodeRawToImageRgba32Async(face.MipMaps[0].Data,
|
2022-12-28 16:21:58 +00:00
|
|
|
(int)face.Width, (int)face.Height, ToCompressionFormat((DXGI_FORMAT)origFormat), token);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-12-28 16:21:58 +00:00
|
|
|
data.Mutate(x => x.Resize(width, height, KnownResamplers.Welch));
|
|
|
|
faces.Add(data);
|
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
|
|
|
|
var encoder = new BcEncoder
|
|
|
|
{
|
|
|
|
OutputOptions =
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
Quality = CompressionQuality.Balanced,
|
|
|
|
GenerateMipMaps = true,
|
|
|
|
Format = ToCompressionFormat(format),
|
|
|
|
FileFormat = OutputFileFormat.Dds
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
2021-10-23 16:51:17 +00:00
|
|
|
};
|
2023-01-21 19:36:12 +00:00
|
|
|
|
2022-12-28 16:21:58 +00:00
|
|
|
switch (faces.Count)
|
|
|
|
{
|
|
|
|
case 1:
|
|
|
|
(await encoder.EncodeToDdsAsync(faces[0], token)).Write(output);
|
|
|
|
break;
|
|
|
|
case 6:
|
|
|
|
(await encoder.EncodeCubeMapToDdsAsync(faces[0], faces[1], faces[2], faces[3], faces[4], faces[5], token))
|
|
|
|
.Write(output);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new NotImplementedException($"Can't encode dds with {faces.Count} faces");
|
|
|
|
}
|
2023-01-21 19:36:12 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
if (!leaveOpen)
|
|
|
|
await output.DisposeAsync();
|
|
|
|
}
|
2023-01-21 19:36:12 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public static CompressionFormat ToCompressionFormat(DXGI_FORMAT dx)
|
|
|
|
{
|
|
|
|
return dx switch
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
DXGI_FORMAT.BC1_UNORM => CompressionFormat.Bc1,
|
|
|
|
DXGI_FORMAT.BC2_UNORM => CompressionFormat.Bc2,
|
|
|
|
DXGI_FORMAT.BC3_UNORM => CompressionFormat.Bc3,
|
|
|
|
DXGI_FORMAT.BC4_UNORM => CompressionFormat.Bc4,
|
|
|
|
DXGI_FORMAT.BC5_UNORM => CompressionFormat.Bc5,
|
|
|
|
DXGI_FORMAT.BC7_UNORM => CompressionFormat.Bc7,
|
2022-08-09 11:54:21 +00:00
|
|
|
DXGI_FORMAT.B8G8R8A8_UNORM => CompressionFormat.Bgra,
|
2022-08-12 23:03:35 +00:00
|
|
|
DXGI_FORMAT.R8G8B8A8_UNORM => CompressionFormat.Rgba,
|
2021-10-23 16:51:17 +00:00
|
|
|
_ => throw new Exception($"Cannot re-encode texture with {dx} format, encoding not supported")
|
|
|
|
};
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public class ImageBitmap : IByteImage
|
|
|
|
{
|
|
|
|
private readonly Image<Rgba32> _image;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public ImageBitmap(Image<Rgba32> image)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
_image = image;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public int Width => _image.Width;
|
|
|
|
public int Height => _image.Height;
|
|
|
|
|
|
|
|
public byte this[int x, int y] => _image[x, y].R;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
}
|