/// <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()); }
/// <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()); }
/// <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); }
/// <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)); }