using System.Buffers; using System.Buffers.Binary; using System.IO.Compression; using System.IO.Pipelines; using StitchATon2.Infra.Buffers; namespace StitchATon2.Infra.Encoders; public class PngPipeEncoder : IDisposable { private const int BufferSize = 8 * 1024; private const int FlushThreshold = 1024; private const int PipeChunkThreshold = 16 * 1024; private readonly PipeWriter _outputPipe; private readonly MemoryStream _memoryStream; private readonly int _width; private readonly int _height; private readonly ZLibStream _zlibStream; private bool _disposed; private bool _shouldFlush; public PngPipeEncoder(PipeWriter outputPipe, int width, int height) { _outputPipe = outputPipe; _width = width; _height = height; _memoryStream = new MemoryStream(BufferSize * 2); _zlibStream = new ZLibStream(_memoryStream, CompressionLevel.Optimal, leaveOpen: true); _memoryStream.SetLength(8); _memoryStream.Position = 8; } ~PngPipeEncoder() => Dispose(); public void WriteHeader() { Span headerBytes = [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG Signature 0x00, 0x00, 0x00, 0x0D, // Length // IHDR chunk 0x49, 0x48, 0x44, 0x52, // IHDR 0x00, 0x00, 0x00, 0x00, // Reserve to write Width 0x00, 0x00, 0x00, 0x00, // Reserve to write Height 0x08, // Bit depth 0x02, // Color type 0x00, // Compression method 0x00, // Filter method 0x00, // Interlace method 0x00, 0x00, 0x00, 0x00, // Reserve to write CRC-32 ]; BinaryPrimitives.WriteInt32BigEndian(headerBytes[16..], _width); BinaryPrimitives.WriteInt32BigEndian(headerBytes[20..], _height); var crc = Crc32.Compute(headerBytes.Slice(12, 17)); BinaryPrimitives.WriteUInt32BigEndian(headerBytes[29..], crc); _outputPipe.Write(headerBytes); } public async Task WriteDataAsync(IBuffer buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default) { _zlibStream.Write([0]); var offset = 0; while (buffer.Length - offset > FlushThreshold) { _zlibStream.Write(buffer.Span.Slice(offset, FlushThreshold)); await _zlibStream.FlushAsync(cancellationToken); offset += FlushThreshold; if(_outputPipe.UnflushedBytes >= PipeChunkThreshold) await FlushAsync(cancellationToken); } if (buffer.Length > offset) { _zlibStream.Write(buffer.Span[offset..]); await _zlibStream.FlushAsync(cancellationToken); _shouldFlush = true; } if(disposeBuffer) buffer.Dispose(); } private async Task FlushAsync(CancellationToken cancellationToken) { await _zlibStream.FlushAsync(cancellationToken); var dataSize = (int)(_memoryStream.Length - 8); _memoryStream.Write("\0\0\0\0"u8); _memoryStream.Position = 4; _memoryStream.Write("IDAT"u8); var buffer = _memoryStream.GetBuffer(); BinaryPrimitives.WriteInt32BigEndian(buffer, dataSize); // write Crc var crc = Crc32.Compute(buffer.AsSpan(4, dataSize + 4)); BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(dataSize + 8), crc); _outputPipe.Write(buffer.AsSpan(0, dataSize + 12)); await _outputPipe.FlushAsync(cancellationToken); _memoryStream.SetLength(8); _memoryStream.Position = 8; _shouldFlush = false; } public async Task WriteEndOfFileAsync(CancellationToken cancellationToken = default) { if(_shouldFlush) await FlushAsync(cancellationToken); Span endChunk = [ 0x00, 0x00, 0x00, 0x00, // Length 0x49, 0x45, 0x4E, 0x44, // IEND 0xAE, 0x42, 0x60, 0x82, // Crc ]; _outputPipe.Write(endChunk); Dispose(); } public void Dispose() { if (!_disposed) { _zlibStream.Dispose(); _memoryStream.Dispose(); _disposed = true; GC.SuppressFinalize(this); } } }