Example #1
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());
        }
Example #2
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());
        }
Example #3
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);
        }
Example #4
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));
        }