PNG writer for the editor. It builds a minimal RGBA8 PNG file with IHDR, IDAT and IEND chunks, writes zlib-wrapped deflate-compressed image data and computes Adler-32 and CRC32 checksums without external dependencies.
using System;
using System.IO;
using System.IO.Compression;
namespace GmodAssetImporter;
/// <summary>
/// Minimal RGBA8 PNG writer (no external deps).
/// </summary>
public static class PngWriter
{
static readonly byte[] Signature = { 137, 80, 78, 71, 13, 10, 26, 10 };
public static void Write( string path, int width, int height, byte[] rgba )
{
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
using var fs = File.Create( path );
fs.Write( Signature );
WriteChunk( fs, "IHDR", BuildIhdr( width, height ) );
WriteChunk( fs, "IDAT", BuildIdat( width, height, rgba ) );
WriteChunk( fs, "IEND", Array.Empty<byte>() );
}
static byte[] BuildIhdr( int width, int height )
{
using var ms = new MemoryStream( 13 );
using var bw = new BinaryWriter( ms );
WriteInt32Be( bw, width );
WriteInt32Be( bw, height );
bw.Write( (byte)8 ); // bit depth
bw.Write( (byte)6 ); // RGBA
bw.Write( (byte)0 );
bw.Write( (byte)0 );
bw.Write( (byte)0 );
return ms.ToArray();
}
static byte[] BuildIdat( int width, int height, byte[] rgba )
{
var raw = new byte[(width * 4 + 1) * height];
for ( var y = 0; y < height; y++ )
{
var rowStart = y * (width * 4 + 1);
raw[rowStart] = 0; // filter none
Buffer.BlockCopy( rgba, y * width * 4, raw, rowStart + 1, width * 4 );
}
using var ms = new MemoryStream();
// zlib wrapper: CMF/FLG + deflate + adler32
ms.WriteByte( 0x78 );
ms.WriteByte( 0x01 );
using ( var deflate = new DeflateStream( ms, CompressionLevel.Optimal, leaveOpen: true ) )
{
deflate.Write( raw, 0, raw.Length );
}
var adler = Adler32( raw );
ms.WriteByte( (byte)((adler >> 24) & 0xFF) );
ms.WriteByte( (byte)((adler >> 16) & 0xFF) );
ms.WriteByte( (byte)((adler >> 8) & 0xFF) );
ms.WriteByte( (byte)(adler & 0xFF) );
return ms.ToArray();
}
static void WriteChunk( Stream stream, string type, byte[] data )
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter( ms );
WriteInt32Be( bw, data.Length );
var typeBytes = System.Text.Encoding.ASCII.GetBytes( type );
bw.Write( typeBytes );
bw.Write( data );
var bytes = ms.ToArray();
stream.Write( bytes, 0, bytes.Length );
var crc = 0xFFFFFFFFu;
crc = Crc32Update( crc, typeBytes, 0, typeBytes.Length );
crc = Crc32Update( crc, data, 0, data.Length );
spanWriteInt32Be( stream, (int)(crc ^ 0xFFFFFFFFu) );
}
static void spanWriteInt32Be( Stream stream, int value )
{
stream.WriteByte( (byte)((value >> 24) & 0xFF) );
stream.WriteByte( (byte)((value >> 16) & 0xFF) );
stream.WriteByte( (byte)((value >> 8) & 0xFF) );
stream.WriteByte( (byte)(value & 0xFF) );
}
static void WriteInt32Be( BinaryWriter bw, int value )
{
bw.Write( (byte)((value >> 24) & 0xFF) );
bw.Write( (byte)((value >> 16) & 0xFF) );
bw.Write( (byte)((value >> 8) & 0xFF) );
bw.Write( (byte)(value & 0xFF) );
}
static uint Adler32( byte[] data )
{
uint a = 1, b = 0;
foreach ( var t in data )
{
a = (a + t) % 65521;
b = (b + a) % 65521;
}
return (b << 16) | a;
}
static readonly uint[] CrcTable = CreateCrcTable();
static uint[] CreateCrcTable()
{
var table = new uint[256];
for ( uint n = 0; n < 256; n++ )
{
var c = n;
for ( var k = 0; k < 8; k++ )
c = (c & 1) != 0 ? 0xEDB88320u ^ (c >> 1) : c >> 1;
table[n] = c;
}
return table;
}
static uint Crc32Update( uint crc, byte[] data, int offset, int length )
{
for ( var i = 0; i < length; i++ )
crc = CrcTable[(crc ^ data[offset + i]) & 0xFF] ^ (crc >> 8);
return crc;
}
}