示例#1
0
        private static Span <byte> ConvertBitArray(Span <byte> bytes, PngFileHeader header)
        {
            int bits = header.BitDepth;

            if (bits == 8)
            {
                return(bytes);
            }
            if (bits < 8)
            {
                return(ImageUtil.ToArrayByBitsLength(bytes, bits, header.ColorType != 3));
            }
            if (bits > 16)
            {
                return(bytes);
            }

            Span <ushort> conv      = MemoryMarshal.Cast <byte, ushort>(bytes);
            Span <byte>   byteArray = new byte[conv.Length];

            for (var i = 0; i < byteArray.Length; i++)
            {
                // Simulate a 8bit display by clipping everything above 255.
                byteArray[i] = (byte)conv[i];
            }

            return(byteArray);
        }
示例#2
0
        private static int GetScanlineLength(int width, PngFileHeader fileHeader)
        {
            int scanlineLength = width * fileHeader.BitDepth * ColorTypes[fileHeader.ColorType].ChannelsPerColor;
            int amount         = scanlineLength % 8;

            if (amount != 0)
            {
                scanlineLength += 8 - amount;
            }
            scanlineLength /= 8;
            return(scanlineLength);
        }
示例#3
0
        private static int GetScanlineLengthInterlaced(int columns, PngFileHeader fileHeader, int channelsPerColor)
        {
            int scanlineLength = columns * fileHeader.BitDepth * channelsPerColor;
            int amount         = scanlineLength % 8;

            if (amount != 0)
            {
                scanlineLength += 8 - amount;
            }
            scanlineLength /= 8;
            return(scanlineLength);
        }
示例#4
0
        private static int GetScanlineLength(PngFileHeader fileHeader, int channelsPerColor)
        {
            int scanlineLength = (int)fileHeader.Size.X * fileHeader.BitDepth * channelsPerColor;
            int amount         = scanlineLength % 8;

            if (amount != 0)
            {
                scanlineLength += 8 - amount;
            }
            scanlineLength /= 8;
            return(scanlineLength);
        }
示例#5
0
        private static void ParseInterlaced(byte[] data, PngFileHeader fileHeader, int bytesPerPixel, IColorReader reader, byte[] pixels)
        {
            const int passes     = 7;
            var       readOffset = 0;
            var       done       = false;
            var       r          = new Span <byte>(data);

            for (var i = 0; i < passes; i++)
            {
                int columns = Adam7.ComputeColumns(fileHeader.Width, i);
                if (columns == 0)
                {
                    continue;
                }
                int rowSize = GetScanlineLength(columns, fileHeader) + 1;

                // Read rows.
                Span <byte> prevRowData = null;
                for (int row = Adam7.FirstRow[i]; row < fileHeader.Height; row += Adam7.RowIncrement[i])
                {
                    // Early out if invalid pass.
                    if (r.Length - readOffset < rowSize)
                    {
                        done = true;
                        break;
                    }

                    var rowData = new Span <byte>(new byte[rowSize]);
                    r.Slice(readOffset, rowSize).CopyTo(rowData);
                    int filter = rowData[0];
                    rowData     = rowData.Slice(1);
                    readOffset += rowSize;

                    // Apply filter to the whole row.
                    for (var column = 0; column < rowData.Length; column++)
                    {
                        rowData[column] = ApplyFilter(rowData, prevRowData, filter, column, bytesPerPixel);
                    }

                    reader.ReadScanlineInterlaced(rowData, pixels, fileHeader, row, i);
                    prevRowData = rowData;
                }

                if (done)
                {
                    break;
                }
            }
        }
示例#6
0
        /// <summary>
        /// Encodes the provided BGRA, Y flipped, pixel data to a PNG image.
        /// </summary>
        /// <param name="pixels">The pixel date to encode.</param>
        /// <param name="width">The width of the image in the data.</param>
        /// <param name="height">The height of the image in the data.</param>
        /// <returns>A PNG image as bytes.</returns>
        public static byte[] Encode(byte[] pixels, int width, int height)
        {
            var       pngHeader    = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
            const int maxBlockSize = 0xFFFF;

            using var stream = new MemoryStream();

            // Write the png header.
            stream.Write(pngHeader, 0, 8);

            var header = new PngFileHeader
            {
                Width             = width,
                Height            = height,
                ColorType         = 6,
                BitDepth          = 8,
                FilterMethod      = 0,
                CompressionMethod = 0,
                InterlaceMethod   = 0
            };

            // Write header chunk.
            var chunkData = new byte[13];

            WriteInteger(chunkData, 0, header.Width);
            WriteInteger(chunkData, 4, header.Height);

            chunkData[8]  = header.BitDepth;
            chunkData[9]  = header.ColorType;
            chunkData[10] = header.CompressionMethod;
            chunkData[11] = header.FilterMethod;
            chunkData[12] = header.InterlaceMethod;

            WriteChunk(stream, PngChunkTypes.HEADER, chunkData);

            // Write data chunks.
            var data = new byte[width * height * 4 + height];

            int rowLength = width * 4 + 1;

            for (var y = 0; y < height; y++)
            {
                data[y * rowLength] = 0;

                for (var x = 0; x < width; x++)
                {
                    // Calculate the offset for the new array.
                    int dataOffset = y * rowLength + x * 4 + 1;

                    // Calculate the offset for the original pixel array.
                    int pixelOffset = (y * width + x) * 4;

                    data[dataOffset + 0] = pixels[pixelOffset + 2];
                    data[dataOffset + 1] = pixels[pixelOffset + 1];
                    data[dataOffset + 2] = pixels[pixelOffset + 0];
                    data[dataOffset + 3] = pixels[pixelOffset + 3];
                }
            }

            byte[] buffer    = ZlibStreamUtility.Compress(data, 6);
            int    numChunks = buffer.Length / maxBlockSize;

            if (buffer.Length % maxBlockSize != 0)
            {
                numChunks++;
            }

            for (var i = 0; i < numChunks; i++)
            {
                int length = buffer.Length - i * maxBlockSize;

                if (length > maxBlockSize)
                {
                    length = maxBlockSize;
                }
                WriteChunk(stream, PngChunkTypes.DATA, buffer, i * maxBlockSize, length);
            }

            // Write end chunk.
            WriteChunk(stream, PngChunkTypes.END, null);

            stream.Flush();
            return(stream.ToArray());
        }
示例#7
0
        private static void Parse(byte[] data, PngFileHeader fileHeader, int bytesPerPixel, IColorReader reader, byte[] pixels)
        {
            // Find the scan line length.
            int scanlineLength = GetScanlineLength(fileHeader.Width, fileHeader) + 1;
            int scanLineCount  = data.Length / scanlineLength;

            // Run through all scanlines.
            var cannotParallel = new List <int>();

            ParallelWork.FastLoops(scanLineCount, (start, end) =>
            {
                int readOffset = start * scanlineLength;
                for (int i = start; i < end; i++)
                {
                    // Early out for invalid data.
                    if (data.Length - readOffset < scanlineLength)
                    {
                        break;
                    }

                    // Get the current scanline.
                    var rowData = new Span <byte>(data, readOffset + 1, scanlineLength - 1);
                    int filter  = data[readOffset];
                    readOffset += scanlineLength;

                    // Check if it has a filter.
                    // PNG filters require the previous row.
                    // We can't do those in parallel.
                    if (filter != 0)
                    {
                        lock (cannotParallel)
                        {
                            cannotParallel.Add(i);
                        }

                        continue;
                    }

                    reader.ReadScanline(rowData, pixels, fileHeader, i);
                }
            }).Wait();

            if (cannotParallel.Count == 0)
            {
                return;
            }

            PerfProfiler.ProfilerEventStart("PNG Parse Sequential", "Loading");

            // Run scanlines which couldn't be parallel processed.
            if (scanLineCount >= 2000)
            {
                Engine.Log.Trace("Loaded a big PNG with scanlines which require filtering. If you re-export it without that, it will load faster.", MessageSource.ImagePng);
            }
            cannotParallel.Sort();
            for (var i = 0; i < cannotParallel.Count; i++)
            {
                int idx = cannotParallel[i];

                int         rowStart    = idx * scanlineLength;
                Span <byte> prevRowData = idx == 0 ? null : new Span <byte>(data, (idx - 1) * scanlineLength + 1, scanlineLength - 1);
                var         rowData     = new Span <byte>(data, rowStart + 1, scanlineLength - 1);

                // Apply filter to the whole row.
                int filter = data[rowStart];
                for (var column = 0; column < rowData.Length; column++)
                {
                    rowData[column] = ApplyFilter(rowData, prevRowData, filter, column, bytesPerPixel);
                }

                reader.ReadScanline(rowData, pixels, fileHeader, idx);
            }

            PerfProfiler.ProfilerEventEnd("PNG Parse Sequential", "Loading");
        }
示例#8
0
        /// <summary>
        /// Decode the provided png file to a BGRA pixel array, Y flipped.
        /// </summary>
        /// <param name="pngData">The png file as bytes.</param>
        /// <param name="fileHeader">The png file's header.</param>
        /// <returns>The RGBA pixel data from the png.</returns>
        public static byte[] Decode(byte[] pngData, out PngFileHeader fileHeader)
        {
            fileHeader = new PngFileHeader();

            if (!IsPng(pngData))
            {
                Engine.Log.Warning("Tried to decode a non-png image!", MessageSource.ImagePng);
                return(null);
            }

            using var stream = new MemoryStream(pngData);
            stream.Seek(8, SeekOrigin.Current);

            var endChunkReached = false;

            byte[] palette      = null;
            byte[] paletteAlpha = null;

            using var dataStream = new MemoryStream();

            // Read chunks while there are valid chunks.
            PngChunk currentChunk;

            while ((currentChunk = new PngChunk(stream)).Valid)
            {
                if (endChunkReached)
                {
                    Engine.Log.Warning("Image did not end with an end chunk...", MessageSource.ImagePng);
                    continue;
                }

                switch (currentChunk.Type)
                {
                case PngChunkTypes.HEADER:
                {
                    Array.Reverse(currentChunk.Data, 0, 4);
                    Array.Reverse(currentChunk.Data, 4, 4);

                    fileHeader.Width  = BitConverter.ToInt32(currentChunk.Data, 0);
                    fileHeader.Height = BitConverter.ToInt32(currentChunk.Data, 4);

                    fileHeader.BitDepth          = currentChunk.Data[8];
                    fileHeader.ColorType         = currentChunk.Data[9];
                    fileHeader.FilterMethod      = currentChunk.Data[11];
                    fileHeader.InterlaceMethod   = currentChunk.Data[12];
                    fileHeader.CompressionMethod = currentChunk.Data[10];

                    // Validate header.
                    if (fileHeader.ColorType >= ColorTypes.Length || ColorTypes[fileHeader.ColorType] == null)
                    {
                        Engine.Log.Warning($"Color type '{fileHeader.ColorType}' is not supported or not valid.", MessageSource.ImagePng);
                        return(null);
                    }

                    if (!ColorTypes[fileHeader.ColorType].SupportedBitDepths.Contains(fileHeader.BitDepth))
                    {
                        Engine.Log.Warning($"Bit depth '{fileHeader.BitDepth}' is not supported or not valid for color type {fileHeader.ColorType}.", MessageSource.ImagePng);
                    }

                    if (fileHeader.FilterMethod != 0)
                    {
                        Engine.Log.Warning("The png specification only defines 0 as filter method.", MessageSource.ImagePng);
                    }
                    break;
                }

                case PngChunkTypes.DATA:
                    dataStream.Write(currentChunk.Data, 0, currentChunk.Data.Length);
                    break;

                case PngChunkTypes.PALETTE:
                    palette = currentChunk.Data;
                    break;

                case PngChunkTypes.PALETTE_ALPHA:
                    paletteAlpha = currentChunk.Data;
                    break;

                case PngChunkTypes.END:
                    endChunkReached = true;
                    break;
                }
            }

            stream.Dispose();

            // Determine color reader to use.
            PngColorTypeInformation colorTypeInformation = ColorTypes[fileHeader.ColorType];

            if (colorTypeInformation == null)
            {
                return(null);
            }
            IColorReader colorReader = colorTypeInformation.CreateColorReader(palette, paletteAlpha);

            // Calculate the bytes per pixel.
            var bytesPerPixel = 1;

            if (fileHeader.BitDepth >= 8)
            {
                bytesPerPixel = colorTypeInformation.ChannelsPerColor * fileHeader.BitDepth / 8;
            }

            // Decompress data.
            dataStream.Position = 0;
            PerfProfiler.ProfilerEventStart("PNG Decompression", "Loading");
            byte[] data = ZlibStreamUtility.Decompress(dataStream);
            PerfProfiler.ProfilerEventEnd("PNG Decompression", "Loading");

            // Parse into pixels.
            var pixels = new byte[fileHeader.Width * fileHeader.Height * 4];

            if (fileHeader.InterlaceMethod == 1)
            {
                ParseInterlaced(data, fileHeader, bytesPerPixel, colorReader, pixels);
            }
            else
            {
                Parse(data, fileHeader, bytesPerPixel, colorReader, pixels);
            }

            return(pixels);
        }
示例#9
0
        private static byte[] ParseInterlaced(byte[] pureData, PngFileHeader fileHeader, int bytesPerPixel, int channelsPerColor, ColorReader reader)
        {
            PerfProfiler.ProfilerEventStart("PNG Parse Interlaced", "Loading");

            // Combine interlaced pixels into one image here.
            var width       = (int)fileHeader.Size.X;
            var height      = (int)fileHeader.Size.Y;
            var combination = new byte[width * height * channelsPerColor];
            var pixels      = new byte[width * height * 4];

            const int passes     = 7;
            var       readOffset = 0;
            var       done       = false;
            var       data       = new Span <byte>(pureData);

            for (var i = 0; i < passes; i++)
            {
                int columns = Adam7.ComputeColumns(width, i);
                if (columns == 0)
                {
                    continue;
                }
                int scanlineLength = GetScanlineLengthInterlaced(columns, fileHeader, channelsPerColor) + 1;
                int length         = scanlineLength - 1;

                int pixelsInLine = Adam7.ComputeBlockWidth(width, i);

                // Read scanlines in this pass.
                var previousScanline = Span <byte> .Empty;
                for (int row = Adam7.FirstRow[i]; row < height; row += Adam7.RowIncrement[i])
                {
                    // Early out if invalid pass.
                    if (data.Length - readOffset < scanlineLength)
                    {
                        done = true;
                        break;
                    }

                    Span <byte> scanLine = data.Slice(readOffset + 1, length);
                    int         filter   = data[readOffset];
                    readOffset += scanlineLength;
                    ApplyFilter(scanLine, previousScanline, filter, bytesPerPixel);

                    // Dump the row into the combined image.
                    Span <byte> convertedLine = ConvertBitArray(scanLine, fileHeader);
                    for (var pixel = 0; pixel < pixelsInLine; pixel++)
                    {
                        int offset    = row * bytesPerPixel * width + (Adam7.FirstColumn[i] + pixel * Adam7.ColumnIncrement[i]) * bytesPerPixel;
                        int offsetSrc = pixel * bytesPerPixel;
                        for (var p = 0; p < bytesPerPixel; p++)
                        {
                            combination[offset + p] = convertedLine[offsetSrc + p];
                        }
                    }

                    previousScanline = scanLine;
                }

                if (done)
                {
                    break;
                }
            }

            // Read the combined image.
            int stride        = width * bytesPerPixel;
            int scanlineCount = combination.Length / stride;

            for (var i = 0; i < scanlineCount; i++)
            {
                Span <byte> row = new Span <byte>(combination).Slice(stride * i, stride);
                reader(width, row, pixels, i);
            }

            PerfProfiler.ProfilerEventEnd("PNG Parse Interlaced", "Loading");
            return(pixels);
        }
示例#10
0
        /// <summary>
        /// Encodes the provided Y flipped pixel data to a PNG image.
        /// </summary>
        /// <param name="pixels">The pixel date to encode.</param>
        /// <param name="size">The size of the image..</param>
        /// <param name="format">The format of the pixel data.</param>
        /// <returns>A PNG image as bytes.</returns>
        public static byte[] Encode(byte[] pixels, Vector2 size, PixelFormat format)
        {
            using var stream = new MemoryStream();

            stream.Write(_pngHeader, 0, 8);

            // Write header chunk.
            var chunkData = new byte[13];
            var width     = (int)size.X;
            var height    = (int)size.Y;

            WriteInteger(chunkData, 0, width);
            WriteInteger(chunkData, 4, height);
            var header = new PngFileHeader
            {
                Size              = size,
                ColorType         = (byte)(format == PixelFormat.Red ? 0 : 6), // Greyscale, otherwise RGBA
                BitDepth          = 8,
                FilterMethod      = 0,
                CompressionMethod = 0,
                InterlaceMethod   = 0,
                PixelFormat       = format
            };

            chunkData[8]  = header.BitDepth;
            chunkData[9]  = header.ColorType;
            chunkData[10] = header.CompressionMethod;
            chunkData[11] = header.FilterMethod;
            chunkData[12] = header.InterlaceMethod;
            WriteChunk(stream, PngChunkTypes.HEADER, chunkData);

            // Write data chunks.
            int bytesPerPixel = Gl.PixelFormatToComponentCount(format);
            var data          = new byte[width * height * bytesPerPixel + height];
            int rowLength     = width * bytesPerPixel + 1; // One byte for the filter

            for (var y = 0; y < height; y++)
            {
                data[y * rowLength] = 0;

                for (var x = 0; x < width; x++)
                {
                    // Calculate the offset for the new array.
                    int dataOffset = y * rowLength + x * bytesPerPixel + 1;

                    // Calculate the offset for the original pixel array.
                    int pixelOffset = (y * width + x) * bytesPerPixel;

                    if (format == PixelFormat.Rgba)
                    {
                        data[dataOffset + 0] = pixels[pixelOffset + 0];
                        data[dataOffset + 1] = pixels[pixelOffset + 1];
                        data[dataOffset + 2] = pixels[pixelOffset + 2];
                        data[dataOffset + 3] = pixels[pixelOffset + 3];
                    }
                    else if (format == PixelFormat.Bgra)
                    {
                        data[dataOffset + 0] = pixels[pixelOffset + 2];
                        data[dataOffset + 1] = pixels[pixelOffset + 1];
                        data[dataOffset + 2] = pixels[pixelOffset + 0];
                        data[dataOffset + 3] = pixels[pixelOffset + 3];
                    }
                    else if (format == PixelFormat.Red)
                    {
                        data[dataOffset] = pixels[pixelOffset];
                    }
                    else
                    {
                        throw new Exception($"Unsupported encoding pixel format - {format}");
                    }
                }
            }

            // Compress data.
            byte[] buffer    = ZlibStreamUtility.Compress(data, 6);
            int    numChunks = buffer.Length / MAX_BLOCK_SIZE;

            if (buffer.Length % MAX_BLOCK_SIZE != 0)
            {
                numChunks++;
            }
            for (var i = 0; i < numChunks; i++)
            {
                int length = buffer.Length - i * MAX_BLOCK_SIZE;
                if (length > MAX_BLOCK_SIZE)
                {
                    length = MAX_BLOCK_SIZE;
                }
                WriteChunk(stream, PngChunkTypes.DATA, buffer, i * MAX_BLOCK_SIZE, length);
            }

            // Write end chunk.
            WriteChunk(stream, PngChunkTypes.END, null);
            stream.Flush();
            return(stream.ToArray());
        }
示例#11
0
        private static byte[] Parse(int scanlineLength, int scanlineCount, byte[] pureData, int bytesPerPixel, PngFileHeader header, ColorReader reader, int bytesPerPixelOutput)
        {
            var width  = (int)header.Size.X;
            var height = (int)header.Size.Y;
            var pixels = new byte[width * height * bytesPerPixelOutput];
            int length = scanlineLength - 1;
            var data   = new Span <byte>(pureData);

            // Analyze if the scanlines can be read in parallel.
            byte filterMode = data[0];

            for (var i = 0; i < scanlineCount; i++)
            {
                byte f = data[scanlineLength * i];
                if (f == filterMode)
                {
                    continue;
                }
                filterMode = byte.MaxValue;
                break;
            }

            // Multiple filters or a dependency filter are in affect.
            if (filterMode == byte.MaxValue || filterMode != 0 && filterMode != 1)
            {
                if (scanlineCount >= 1500)
                {
                    Engine.Log.Trace("Loaded a big PNG with scanlines which require filtering. If you re-export it without filters, it will load faster.", MessageSource.ImagePng);
                }

                PerfProfiler.ProfilerEventStart("PNG Parse Sequential", "Loading");
                var readOffset       = 0;
                var previousScanline = Span <byte> .Empty;
                for (var i = 0; i < scanlineCount; i++)
                {
                    // Early out for invalid data.
                    if (data.Length - readOffset < scanlineLength)
                    {
                        break;
                    }

                    Span <byte> scanline = data.Slice(readOffset + 1, length);
                    int         filter   = data[readOffset];
                    ApplyFilter(scanline, previousScanline, filter, bytesPerPixel);

                    reader(width, ConvertBitArray(scanline, header), pixels, i);
                    previousScanline = scanline;
                    readOffset      += scanlineLength;
                }

                PerfProfiler.ProfilerEventEnd("PNG Parse Sequential", "Loading");
                return(pixels);
            }

            // Single line filter
            if (filterMode == 1)
            {
                PerfProfiler.ProfilerEventStart("PNG Parse Threaded", "Loading");
                ParallelWork.FastLoops(scanlineCount, (start, end) =>
                {
                    int readOffset = start * scanlineLength;
                    for (int i = start; i < end; i++)
                    {
                        // Early out for invalid data.
                        if (pureData.Length - readOffset < scanlineLength)
                        {
                            break;
                        }
                        Span <byte> scanline = new Span <byte>(pureData).Slice(readOffset + 1, length);
                        for (int j = bytesPerPixel; j < scanline.Length; j++)
                        {
                            scanline[j] = (byte)(scanline[j] + scanline[j - bytesPerPixel]);
                        }

                        reader(width, ConvertBitArray(scanline, header), pixels, i);
                        readOffset += scanlineLength;
                    }
                }).Wait();
                PerfProfiler.ProfilerEventEnd("PNG Parse Threaded", "Loading");
            }

            // No filter!
            // ReSharper disable once InvertIf
            if (filterMode == 0)
            {
                PerfProfiler.ProfilerEventStart("PNG Parse Threaded", "Loading");
                ParallelWork.FastLoops(scanlineCount, (start, end) =>
                {
                    int readOffset = start * scanlineLength;
                    for (int i = start; i < end; i++)
                    {
                        // Early out for invalid data.
                        if (pureData.Length - readOffset < scanlineLength)
                        {
                            break;
                        }
                        Span <byte> row = ConvertBitArray(new Span <byte>(pureData).Slice(readOffset + 1, length), header);
                        reader(width, row, pixels, i);
                        readOffset += scanlineLength;
                    }
                }).Wait();
                PerfProfiler.ProfilerEventEnd("PNG Parse Threaded", "Loading");
            }

            return(pixels);
        }
示例#12
0
        /// <summary>
        /// Decode the provided png file to a BGRA pixel array, Y flipped.
        /// </summary>
        /// <param name="pngData">The png file as bytes.</param>
        /// <param name="fileHeader">The png file's header.</param>
        /// <returns>The RGBA pixel data from the png.</returns>
        public static byte[] Decode(ReadOnlyMemory <byte> pngData, out PngFileHeader fileHeader)
        {
            fileHeader = new PngFileHeader();
            if (!IsPng(pngData))
            {
                Engine.Log.Warning("Tried to decode a non-png image!", MessageSource.ImagePng);
                return(null);
            }

            using var stream = new ByteReader(pngData);
            stream.Seek(8, SeekOrigin.Current); // Increment by header bytes.

            // Read chunks while there are valid chunks.
            var      dataStream = new ReadOnlyLinkedMemoryStream();
            PngChunk currentChunk;
            var      endChunkReached = false;
            ReadOnlyMemory <byte> palette = null, paletteAlpha = null;
            int width = 0, height = 0;

            while ((currentChunk = new PngChunk(stream)).Valid)
            {
                if (endChunkReached)
                {
                    Engine.Log.Warning("Image did not end with an end chunk...", MessageSource.ImagePng);
                    continue;
                }

                switch (currentChunk.Type)
                {
                case PngChunkTypes.HEADER:
                {
                    ByteReader chunkReader = currentChunk.ChunkReader;

                    width                        = chunkReader.ReadInt32BE();
                    height                       = chunkReader.ReadInt32BE();
                    fileHeader.Size              = new Vector2(width, height);
                    fileHeader.BitDepth          = chunkReader.ReadByte();
                    fileHeader.ColorType         = chunkReader.ReadByte();
                    fileHeader.CompressionMethod = chunkReader.ReadByte();
                    fileHeader.FilterMethod      = chunkReader.ReadByte();
                    fileHeader.InterlaceMethod   = chunkReader.ReadByte();
                    break;
                }

                case PngChunkTypes.DATA:
                    dataStream.AddMemory(currentChunk.ChunkReader.Data);
                    break;

                case PngChunkTypes.PALETTE:
                    palette = currentChunk.ChunkReader.Data;
                    break;

                case PngChunkTypes.PALETTE_ALPHA:
                    paletteAlpha = currentChunk.ChunkReader.Data;
                    break;

                case PngChunkTypes.END:
                    endChunkReached = true;
                    break;
                }
            }

            // Decompress data.
            PerfProfiler.ProfilerEventStart("PNG Decompression", "Loading");
            byte[] data = ZlibStreamUtility.Decompress(dataStream);
            PerfProfiler.ProfilerEventEnd("PNG Decompression", "Loading");
            if (data == null)
            {
                return(null);
            }

            var         channelsPerColor    = 0;
            int         bytesPerPixelOutput = 4;
            ColorReader reader;

            fileHeader.PixelFormat = PixelFormat.Bgra; // Default.
            switch (fileHeader.ColorType)
            {
            case 0:
                // Grayscale - No Alpha
                channelsPerColor    = 1;
                bytesPerPixelOutput = 1;
                reader = Grayscale;
                fileHeader.PixelFormat = PixelFormat.Red;
                break;

            case 2:
                // RGB
                channelsPerColor = 3;
                reader           = Rgb;
                break;

            case 3:
                // Palette
                channelsPerColor = 1;
                reader           = (rowPixels, row, imageDest, destRow) => { Palette(palette.Span, paletteAlpha.Span, rowPixels, row, imageDest, destRow); };
                break;

            case 4:
                // Grayscale - Alpha
                channelsPerColor = 2;
                reader           = GrayscaleAlpha;
                break;

            case 6:
                // RGBA
                channelsPerColor       = 4;
                reader                 = Rgba;
                fileHeader.PixelFormat = PixelFormat.Rgba;
                break;

            default:
                reader = null;
                break;
            }

            // Unknown color mode.
            if (reader == null)
            {
                Engine.Log.Warning($"Unsupported color type - {fileHeader.ColorType}", MessageSource.ImagePng);
                return(new byte[width * height * bytesPerPixelOutput]);
            }

            // Calculate the bytes per pixel.
            var bytesPerPixel = 1;

            if (fileHeader.BitDepth == 0 || fileHeader.BitDepth == 3)
            {
                Engine.Log.Warning("Invalid bit depth.", MessageSource.ImagePng);
                return(null);
            }
            if (fileHeader.BitDepth != 8)
            {
                Engine.Log.Warning("Loading PNGs with a bit depth different than 8 will be deprecated in future versions.", MessageSource.ImagePng);
            }
            if (fileHeader.BitDepth >= 8)
            {
                bytesPerPixel = channelsPerColor * fileHeader.BitDepth / 8;
            }

            // Check interlacing.
            if (fileHeader.InterlaceMethod == 1)
            {
                Engine.Log.Warning("Loading interlaced PNGs will be deprecated in future versions. Convert your images!", MessageSource.ImagePng);
                return(ParseInterlaced(data, fileHeader, bytesPerPixel, channelsPerColor, reader));
            }

            int scanlineLength = GetScanlineLength(fileHeader, channelsPerColor) + 1;
            int scanLineCount  = data.Length / scanlineLength;

            return(Parse(scanlineLength, scanLineCount, data, bytesPerPixel, fileHeader, reader, bytesPerPixelOutput));
        }