Created
June 7, 2025 23:20
-
-
Save exelix11/0732fbb4aedd9beb8e123eef0ace13d3 to your computer and use it in GitHub Desktop.
This script decodes the custom ZNG image format found in certain renpy games on switch. It uses a custom decompression algorithm which seems nobody REd before..
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Buffers.Binary; | |
using System.Drawing.Imaging; | |
ConvertImage("test.png", "output.png"); | |
void ConvertImage(string input, string output) | |
{ | |
var data = File.ReadAllBytes(input); | |
data = DecodeFile(data, true); | |
data = DecodeFile(data, false); // Double decompression?? | |
var bpp = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(0, 4)); | |
var width = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(4, 4)); | |
var height = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(8, 4)); | |
if (bpp != 32) | |
throw new Exception("Unsupported bits per pixel: " + bpp); | |
var imageData = data.AsSpan(12); | |
var bitmap = MakePng(width, height, imageData); | |
File.WriteAllBytes(output, bitmap); | |
} | |
byte[] DecodeFile(Span<byte> file, bool expectImage) | |
{ | |
if (expectImage) | |
{ | |
// File magic is 7A 00 00 00 | |
if (BinaryPrimitives.ReadUInt32LittleEndian(file) != 0x7A) | |
throw new Exception("Image header not found"); | |
file = file.Slice(4); | |
} | |
var originalSize = BinaryPrimitives.ReadInt32LittleEndian(file); | |
var compressedSize = BinaryPrimitives.ReadInt32LittleEndian(file.Slice(4)); | |
file = file.Slice(8); | |
var output = new byte[originalSize]; | |
if (compressedSize > file.Length) | |
throw new Exception("Compressed size is less than the data length"); | |
DecompressAlgo(new BinaryReader(new MemoryStream(file.Slice(0, compressedSize).ToArray())), output); | |
return output; | |
} | |
bool EOF(BinaryReader input) => input.BaseStream.Position >= input.BaseStream.Length; | |
int ReadMultibyteLength(BinaryReader input) | |
{ | |
var length = 0; | |
while (!EOF(input)) | |
{ | |
var val = input.ReadByte(); | |
length += val; | |
if (val != 0xFF) | |
break; | |
}; | |
return length; | |
} | |
void DecompressAlgo(BinaryReader input, Span<byte> output) | |
{ | |
int outputOffset = 0; | |
int backAmount = 0; | |
int back = 0; | |
int repeat = 0; | |
while (!EOF(input)) | |
{ | |
var value = input.ReadByte(); | |
var high = (value >> 4); | |
repeat = (value & 0xF); | |
if (high == 0xF) | |
high = ReadMultibyteLength(input) + 15; | |
if (high != 0) | |
{ | |
input.ReadBytes(high).CopyTo(output.Slice(outputOffset)); | |
outputOffset += high; | |
} | |
if (EOF(input)) | |
break; | |
backAmount = input.ReadUInt16(); | |
back = outputOffset - backAmount; | |
if (back < 0) | |
return; | |
if (repeat == 0xF) | |
repeat = ReadMultibyteLength(input) + 15; | |
repeat += 4; | |
// The back reference may point to data we haven't written yet | |
// it's important to use a for loop here instead of slicing the span | |
for (int i = 0; i < repeat; i++) | |
{ | |
output[outputOffset] = output[back + i]; | |
++outputOffset; | |
} | |
} | |
if (!EOF(input)) | |
{ | |
var remaining = input.ReadBytes((int)(input.BaseStream.Length - input.BaseStream.Position)); | |
remaining.CopyTo(output.Slice(outputOffset)); | |
} | |
} | |
byte[] MakePng(int width, int height, Span<byte> data) | |
{ | |
var image = new Bitmap(width, height); | |
for (int y = 0; y < height; y++) | |
{ | |
for (int x = 0; x < width; x++) | |
{ | |
var index = (y * width + x) * 4; | |
var r = data[index + 0]; | |
var g = data[index + 1]; | |
var b = data[index + 2]; | |
var a = data[index + 3]; | |
image.SetPixel(x, y, Color.FromArgb(a, r, g, b)); | |
} | |
} | |
using var ms = new MemoryStream(); | |
image.Save(ms, ImageFormat.Png); | |
return ms.ToArray(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment