Editor/Conversion/Writers/PngWriter.cs

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.

File AccessNative Interop
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;
	}
}