Skip to content

Instantly share code, notes, and snippets.

@exelix11
Created June 7, 2025 23:20
Show Gist options
  • Save exelix11/0732fbb4aedd9beb8e123eef0ace13d3 to your computer and use it in GitHub Desktop.
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..
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