PNG/PngOpener.cs
2024-11-03 21:34:26 +00:00

158 lines
5.7 KiB
C#

using System.Buffers.Binary;
using System.IO.Compression;
using System.Text;
namespace Uwaa.PNG;
internal static class PngOpener
{
public static Png Open(Stream stream)
{
ArgumentNullException.ThrowIfNull(stream, nameof(stream));
if (!stream.CanRead)
throw new ArgumentException($"The provided stream of type {stream.GetType().FullName} was not readable.");
if (!HasValidHeader(stream))
throw new ArgumentException($"The provided stream did not start with the PNG header.");
Span<byte> crc = stackalloc byte[4];
ImageHeader imageHeader = ReadImageHeader(stream, crc);
bool hasEncounteredImageEnd = false;
Palette? palette = null;
using MemoryStream output = new MemoryStream();
using MemoryStream memoryStream = new MemoryStream();
while (TryReadChunkHeader(stream, out var header))
{
if (hasEncounteredImageEnd)
break;
byte[] bytes = new byte[header.Length];
int read = stream.Read(bytes, 0, bytes.Length);
if (read != bytes.Length)
throw new InvalidOperationException($"Did not read {header.Length} bytes for the {header} header, only found: {read}.");
if (header.IsCritical)
{
switch (header.Name)
{
case "PLTE":
if (header.Length % 3 != 0)
throw new InvalidOperationException($"Palette data must be multiple of 3, got {header.Length}.");
// Ignore palette data unless the header.ColorType indicates that the image is paletted.
if (imageHeader.ColorType.HasFlag(ColorType.PaletteUsed))
palette = new Palette(bytes);
break;
case "IDAT":
memoryStream.Write(bytes, 0, bytes.Length);
break;
case "IEND":
hasEncounteredImageEnd = true;
break;
default:
throw new NotSupportedException($"Encountered critical header {header} which was not recognised.");
}
}
else
{
switch (header.Name)
{
case "tRNS":
// Add transparency to palette, if the PLTE chunk has been read.
palette?.SetAlphaValues(bytes);
break;
}
}
read = stream.Read(crc);
if (read != 4)
throw new InvalidOperationException($"Did not read 4 bytes for the CRC, only found: {read}.");
int result = (int)Crc32.Calculate(Encoding.ASCII.GetBytes(header.Name), bytes);
int crcActual = (crc[0] << 24) + (crc[1] << 16) + (crc[2] << 8) + crc[3];
if (result != crcActual)
throw new InvalidOperationException($"CRC calculated {result} did not match file {crcActual} for chunk: {header.Name}.");
}
memoryStream.Flush();
memoryStream.Seek(2, SeekOrigin.Begin);
using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionMode.Decompress))
{
deflateStream.CopyTo(output);
deflateStream.Close();
}
byte[] bytesOut = output.ToArray();
(byte bytesPerPixel, byte samplesPerPixel) = Decoder.GetBytesAndSamplesPerPixel(imageHeader);
bytesOut = Decoder.Decode(bytesOut, imageHeader, bytesPerPixel, samplesPerPixel);
return new Png(imageHeader, new RawPngData(bytesOut, bytesPerPixel, palette, imageHeader), palette?.HasAlphaValues ?? false);
}
static bool HasValidHeader(Stream stream)
{
for (int i = 0; i < ImageHeader.ValidationHeader.Length; i++)
if (stream.ReadByte() != ImageHeader.ValidationHeader[i])
return false;
return true;
}
static bool TryReadChunkHeader(Stream stream, out ChunkHeader chunkHeader)
{
chunkHeader = default;
long position = stream.Position;
if (!StreamHelper.TryReadHeaderBytes(stream, out var headerBytes))
return false;
int length = BinaryPrimitives.ReadInt32BigEndian(headerBytes);
string name = Encoding.ASCII.GetString(headerBytes, 4, 4);
chunkHeader = new ChunkHeader(position, length, name);
return true;
}
static ImageHeader ReadImageHeader(Stream stream, Span<byte> crc)
{
if (!TryReadChunkHeader(stream, out var header))
throw new ArgumentException("The provided stream did not contain a single chunk.");
if (header.Name != "IHDR")
throw new ArgumentException($"The first chunk was not the IHDR chunk: {header}.");
if (header.Length != 13)
throw new ArgumentException($"The first chunk did not have a length of 13 bytes: {header}.");
byte[] ihdrBytes = new byte[13];
int read = stream.Read(ihdrBytes, 0, ihdrBytes.Length);
if (read != 13)
throw new InvalidOperationException($"Did not read 13 bytes for the IHDR, only found: {read}.");
read = stream.Read(crc);
if (read != 4)
throw new InvalidOperationException($"Did not read 4 bytes for the CRC, only found: {read}.");
int width = BinaryPrimitives.ReadInt32BigEndian(ihdrBytes);
int height = BinaryPrimitives.ReadInt32BigEndian(ihdrBytes[4..]);
byte bitDepth = ihdrBytes[8];
byte colorType = ihdrBytes[9];
byte interlaceMethod = ihdrBytes[12];
return new ImageHeader(width, height, bitDepth, (ColorType)colorType, (InterlaceMethod)interlaceMethod);
}
}