pk3DS/pk3DS.Core/ImageUtil.cs
2024-06-02 18:56:55 -05:00

174 lines
6.8 KiB
C#

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using pk3DS.Core.CTR;
using pk3DS.Core.CTR.Images;
using static pk3DS.Core.CTR.Images.XLIMUtil;
namespace pk3DS.Core;
/// <summary>
/// Image Utility class using <see cref="System.Drawing"/>.
/// </summary>
public static class ImageUtil
{
/// <summary>
/// Converts a <see cref="BFLIM"/> to <see cref="Bitmap"/> via the 32bit/pixel data.
/// </summary>
/// <param name="bflim">Image data</param>
/// <param name="crop">Crop the image area to the actual dimensions</param>
/// <returns>Human visible data</returns>
public static Bitmap GetBitmap(this BXLIM bflim, bool crop = true)
{
if (bflim.Format is XLIMEncoding.ETC1 or XLIMEncoding.ETC1A4)
return GetBitmapETC(bflim, crop);
var data = bflim.GetImageData(crop);
return GetBitmap(data, bflim.Footer.Width, bflim.Footer.Height);
}
public static Bitmap GetBitmap(byte[] data, int width, int height, PixelFormat format = PixelFormat.Format32bppArgb)
{
var bmp = new Bitmap(width, height, format);
var bmpData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, format);
var ptr = bmpData.Scan0;
Marshal.Copy(data, 0, ptr, data.Length);
bmp.UnlockBits(bmpData);
return bmp;
}
public static byte[] GetPixelData(Bitmap bitmap)
{
var argbData = new byte[bitmap.Width * bitmap.Height * 4];
var bd = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, bitmap.PixelFormat);
Marshal.Copy(bd.Scan0, argbData, 0, bitmap.Width * bitmap.Height * 4);
bitmap.UnlockBits(bd);
return argbData;
}
public static Bitmap GetBitmapETC(BXLIM bxlim, bool crop = true)
{
bool etc1a4 = bxlim.Footer.Format == XLIMEncoding.ETC1A4;
byte[] data = bxlim.PixelData;
ETC1.CheckETC1Lib();
try
{
var width = Math.Max(NextLargestPow2(bxlim.Width), 16);
var height = Math.Max(NextLargestPow2(bxlim.Height), 16);
var img = new Bitmap(width, height);
img = DecodeETC(bxlim, img, data, etc1a4);
return crop ? CropBMP(bxlim, img) : img;
}
catch { return null; }
}
public static Bitmap CropBMP(IXLIMHeader bclim, Bitmap img)
{
var cropRect = new Rectangle(0, 0, bclim.Width, bclim.Height);
var src = img;
var target = new Bitmap(cropRect.Width, cropRect.Height);
using (Graphics g = Graphics.FromImage(target))
{
g.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height),
cropRect,
GraphicsUnit.Pixel);
}
Console.WriteLine($"Returning cropped {target.Width}");
return target;
}
private static Bitmap DecodeETC(IXLIMHeader bclim, Bitmap img, byte[] textureData, bool etc1A4)
{
/* http://jul.rustedlogic.net/thread.php?id=17312
* Much of this code is taken/modified from Tharsis. Thank you to Tharsis's creator, xdaniel.
* https://github.com/xdanieldzd/Tharsis
*/
/* Get compressed data & handle to it */
//textureData = switchEndianness(textureData, 0x10);
ushort[] input = new ushort[textureData.Length / sizeof(ushort)];
Buffer.BlockCopy(textureData, 0, input, 0, textureData.Length);
GCHandle pInput = GCHandle.Alloc(input, GCHandleType.Pinned);
/* Marshal data around, invoke ETC1.dll for conversion, etc */
uint size1 = 0;
var w = (ushort)img.Width;
var h = (ushort)img.Height;
ETC1.ConvertETC1(IntPtr.Zero, ref size1, IntPtr.Zero, w, h, etc1A4); // true = etc1a4, false = etc1
uint[] output = new uint[size1];
GCHandle pOutput = GCHandle.Alloc(output, GCHandleType.Pinned);
ETC1.ConvertETC1(pOutput.AddrOfPinnedObject(), ref size1, pInput.AddrOfPinnedObject(), w, h, etc1A4);
pOutput.Free();
pInput.Free();
/* Unscramble if needed // could probably be done in ETC1Lib.dll, it's probably pretty ugly, but whatever... */
/* Non-square code blocks could need some cleanup, verification, etc. as well... */
uint[] finalized = new uint[output.Length];
// Act if it's square because BCLIM swizzling is stupid
Array.Copy(output, 0, finalized, 0, finalized.Length);
byte[] tmp = new byte[finalized.Length];
Buffer.BlockCopy(finalized, 0, tmp, 0, tmp.Length);
byte[] imgData = tmp;
for (int i = 0; i < w; i++)
{
for (int j = 0; j < h; j++)
{
int k = (j + (i * img.Height)) * 4;
img.SetPixel(i, j, Color.FromArgb(imgData[k + 3], imgData[k], imgData[k + 1], imgData[k + 2]));
}
}
if (bclim is BFLIM)
return img;
// Image is 13 instead of 12
// 24 34
img.RotateFlip(RotateFlipType.Rotate90FlipX);
if (w > h)
{
// Image is now in appropriate order, but the shifting is messed up. Let's fix that.
var img2 = new Bitmap(Math.Max(NextLargestPow2(bclim.Width), 16), Math.Max(NextLargestPow2(bclim.Height), 16));
for (int y = 0; y < Math.Max(NextLargestPow2(bclim.Width), 16); y += 8)
{
for (int x = 0; x < Math.Max(NextLargestPow2(bclim.Height), 16); x++)
{
for (int j = 0; j < 8; j++)
// Treat every 8 vertical pixels as 1 pixel for purposes of calculation, add to offset later.
{
int x1 = (x + (y / 8 * h)) % img2.Width; // Reshift x
int y1 = (x + (y / 8 * h)) / img2.Width * 8; // Reshift y
img2.SetPixel(x1, y1 + j, img.GetPixel(x, y + j)); // Reswizzle
}
}
}
return img2;
}
if (h > w)
{
var img2 = new Bitmap(Math.Max(NextLargestPow2(bclim.Width), 16), Math.Max(NextLargestPow2(bclim.Height), 16));
for (int y = 0; y < Math.Max(NextLargestPow2(bclim.Width), 16); y += 8)
{
for (int x = 0; x < Math.Max(NextLargestPow2(bclim.Height), 16); x++)
{
for (int j = 0; j < 8; j++)
// Treat every 8 vertical pixels as 1 pixel for purposes of calculation, add to offset later.
{
int x1 = x % img2.Width; // Reshift x
int y1 = (x + (y / 8 * h)) / img2.Width * 8; // Reshift y
img2.SetPixel(x1, y1 + j, img.GetPixel(x, y + j)); // Reswizzle
}
}
}
return img2;
}
return img;
}
}