diff --git a/.gitignore b/.gitignore index 4f833cd..7f65bd4 100644 --- a/.gitignore +++ b/.gitignore @@ -432,4 +432,5 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ -# End of https://www.toptal.com/developers/gitignore/api/visualstudio,rider \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/visualstudio,rider +/Tests diff --git a/FauFau/Formats/Czi.cs b/FauFau/Formats/Czi.cs new file mode 100644 index 0000000..95a5339 --- /dev/null +++ b/FauFau/Formats/Czi.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Bitter; +using FauFau.Util; +using SharpCompress; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; + +namespace FauFau.Formats +{ + public class Czi : BinaryWrapper + { + public Header Head; + public List MipInfos = new (); + public List CompressedBlocks = new (); + + public Czi() { } + public Czi(string filePath) + { + Load(filePath); + } + + public static Czi CreateMaskCzi(int width, int height) + { + var czi = new Czi(); + czi.Head = new Header + { + Magic = "CZIM", + Version = 3, + Width = width, + Height = height, + Unk = 0, + PatternFlags = 0, + NumMipLevels = 0 + }; + + return czi; + } + + public void Load(string filePath) + { + var data = File.ReadAllBytes(filePath); + using var bs = new BinaryStream(new MemoryStream(data)); + + Read(bs); + } + + public void Save(string path) + { + using var fs = new FileStream(path, FileMode.Create); + Write(fs); + } + + public override void Read(BinaryStream bs) + { + Head = bs.Read.Type
(); + MipInfos = bs.Read.TypeList(Head.NumMipLevels); + CompressedBlocks = bs.Read.TypeList(Head.NumMipLevels); + } + + public override void Write(BinaryStream bs) + { + bs.Write.Type(Head); + bs.Write.TypeList(MipInfos); + bs.Write.TypeList(CompressedBlocks); + } + + public byte[] GetMipDecompressed(int idx) + { + var mipInfo = MipInfos[idx]; + var compressedBlock = CompressedBlocks[idx]; + + byte[] decompressedData = new byte[mipInfo.Size]; + var msIn = new MemoryStream(compressedBlock.Data); + using var zlib = new ZlibStream(msIn, CompressionMode.Decompress); + zlib.ReadFully(decompressedData); + + return decompressedData; + } + + public void AddMipLevel(ReadOnlySpan data) + { + var mipInfo = new MipInfo + { + Offset = Head.NumMipLevels > 0 ? MipInfos[Head.NumMipLevels - 1].Offset + MipInfos[Head.NumMipLevels - 1].Size : 0, + Size = data.Length + }; + + using var inMs = new BinaryStream(new MemoryStream(data.ToArray())); + using var outMs = new MemoryStream(); + using var outBs = new BinaryStream(outMs); + outBs.Write.Byte(0x78); + outBs.Write.Byte(0x9c); + Common.Deflate(inMs, outBs, CompressionLevel.Default); + outBs.ByteOrder = BinaryStream.Endianness.BigEndian; + outBs.Write.UInt(Checksum.Adler32(data)); + outBs.ByteOrder = BinaryStream.Endianness.LittleEndian; + outBs.Flush(); + + var outData = outMs.ToArray(); + + var block = new CompressedBlock() + { + Length = outData.Length, + Data = outData + }; + + MipInfos.Add(mipInfo); + CompressedBlocks.Add(block); + Head.NumMipLevels++; + } + + public class Header : ReadWrite + { + public string Magic; + public int Version; + public int Width; + public int Height; + public byte PatternFlags; + public byte Unk; + public int NumMipLevels; + + public void Read(BinaryStream bs) + { + Magic = bs.Read.String(4); + Version = bs.Read.Int(); + Width = bs.Read.Int(); + Height = bs.Read.Int(); + PatternFlags = bs.Read.Byte(); + Unk = bs.Read.Byte(); + NumMipLevels = bs.Read.Int(); + } + + public void Write(BinaryStream bs) + { + bs.Write.String(Magic); + bs.Write.Int(Version); + bs.Write.Int(Width); + bs.Write.Int(Height); + bs.Write.Byte(PatternFlags); + bs.Write.Byte(Unk); + bs.Write.Int(NumMipLevels); + } + } + + public class MipInfo : ReadWrite + { + public int Offset; // The combined size of all the previous mip infos added + public int Size; + + public void Read(BinaryStream bs) + { + Offset = bs.Read.Int(); + Size = bs.Read.Int(); + } + + public void Write(BinaryStream bs) + { + bs.Write.Int(Offset); + bs.Write.Int(Size); + } + } + + public class CompressedBlock : ReadWrite + { + public int Length; + public byte[] Data; + + public void Read(BinaryStream bs) + { + Length = bs.Read.Int(); + Data = bs.Read.ByteArray(Length); + } + + public void Write(BinaryStream bs) + { + bs.Write.Int(Length); + bs.Write.ByteArray(Data); + } + } + } +} diff --git a/FauFau/Util/Checksum.cs b/FauFau/Util/Checksum.cs index 60d6d65..c512ba2 100644 --- a/FauFau/Util/Checksum.cs +++ b/FauFau/Util/Checksum.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Text; @@ -23,5 +24,16 @@ public static uint FFnv32(byte[] array) return 33U * (hash ^ (hash >> 17)); } } + + public static uint Adler32(ReadOnlySpan data) + { + const int mod = 65521; + uint a = 1, b = 0; + foreach (var c in data) { + a = (a + c) % mod; + b = (b + a) % mod; + } + return (b << 16) | a; + } } } diff --git a/Tests/CziTests.cs b/Tests/CziTests.cs new file mode 100644 index 0000000..6439708 --- /dev/null +++ b/Tests/CziTests.cs @@ -0,0 +1,45 @@ +using System.IO; +using System.Linq; +using FauFau.Formats; + +namespace Tests +{ + public class CziTests + { + + public static void ReadTest() + { + var czi = new Czi("C:\\temp\\FauFau\\00107072_org.czi"); + DumpToFolder(czi, "C:\\temp\\FauFau\\00107072_og"); + + string packDir = "C:\\temp\\FauFau\\00107072_pack"; + if (Directory.Exists(packDir)) { + var newCzi = PackFromFolder(2048, 2048, packDir); + newCzi.Save("C:\\temp\\FauFau\\00107072.czi"); + } + } + + private static void DumpToFolder(Czi czi, string folder) + { + Directory.CreateDirectory(folder); + + for (int i = 0; i < czi.Head.NumMipLevels; i++) { + var mipData = czi.GetMipDecompressed(i); + File.WriteAllBytes($"{folder}\\{i}.raw", mipData); + } + } + + private static Czi PackFromFolder(int width, int height, string folder) + { + var czi = Czi.CreateMaskCzi(width, height); + + var files = Directory.GetFiles(folder).Order(); + foreach (var file in files) { + var data = File.ReadAllBytes(file); + czi.AddMipLevel(data); + } + + return czi; + } + } +} diff --git a/Tests/Program.cs b/Tests/Program.cs index c865cb4..464218a 100644 --- a/Tests/Program.cs +++ b/Tests/Program.cs @@ -23,6 +23,8 @@ static void Main(string[] args) //SDBTests.TestRead(); //SDBTests.TestWriteCustom(); //SDBTests.TestReadCustom(); + + CziTests.ReadTest(); } } } \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 774c66b..761e86e 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -3,7 +3,7 @@ Exe net8.0 - 9 + latestmajor