// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Threading; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png { /// /// Performs the png decoding operation. /// internal sealed class PngDecoderCore : IImageDecoderInternals { /// /// Reusable buffer. /// private readonly byte[] buffer = new byte[4]; /// /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. /// private readonly bool ignoreMetadata; /// /// Used the manage memory allocations. /// private readonly MemoryAllocator memoryAllocator; /// /// The stream to decode from. /// private BufferedReadStream currentStream; /// /// The png header. /// private PngHeader header; /// /// The number of bytes per pixel. /// private int bytesPerPixel; /// /// The number of bytes per sample. /// private int bytesPerSample; /// /// The number of bytes per scanline. /// private int bytesPerScanline; /// /// The palette containing color information for indexed png's. /// private byte[] palette; /// /// The palette containing alpha channel color information for indexed png's. /// private byte[] paletteAlpha; /// /// A value indicating whether the end chunk has been reached. /// private bool isEndChunkReached; /// /// Previous scanline processed. /// private IMemoryOwner previousScanline; /// /// The current scanline that is being processed. /// private IMemoryOwner scanline; /// /// The index of the current scanline being processed. /// private int currentRow = Adam7.FirstRow[0]; /// /// The current number of bytes read in the current scanline. /// private int currentRowBytesRead; /// /// Gets or sets the png color type. /// private PngColorType pngColorType; /// /// The next chunk of data to return. /// private PngChunk? nextChunk; /// /// Initializes a new instance of the class. /// /// The configuration. /// The decoder options. public PngDecoderCore(Configuration configuration, IPngDecoderOptions options) { this.Configuration = configuration ?? Configuration.Default; this.memoryAllocator = this.Configuration.MemoryAllocator; this.ignoreMetadata = options.IgnoreMetadata; } /// public Configuration Configuration { get; } /// /// Gets the dimensions of the image. /// public Size Dimensions => new Size(this.header.Width, this.header.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; this.currentStream.Skip(8); Image image = null; try { while (!this.isEndChunkReached && this.TryReadChunk(out PngChunk chunk)) { try { switch (chunk.Type) { case PngChunkType.Header: this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.Physical: this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Gamma: this.ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.Data: if (image is null) { this.InitializeImage(metadata, out image); } this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata); break; case PngChunkType.Palette: var pal = new byte[chunk.Length]; chunk.Data.GetSpan().CopyTo(pal); this.palette = pal; break; case PngChunkType.Transparency: var alpha = new byte[chunk.Length]; chunk.Data.GetSpan().CopyTo(alpha); this.paletteAlpha = alpha; this.AssignTransparentMarkers(alpha, pngMetadata); break; case PngChunkType.Text: this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.CompressedText: this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.InternationalText: this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.Exif: if (!this.ignoreMetadata) { var exifData = new byte[chunk.Length]; chunk.Data.GetSpan().CopyTo(exifData); metadata.ExifProfile = new ExifProfile(exifData); } break; case PngChunkType.End: this.isEndChunkReached = true; break; case PngChunkType.ProprietaryApple: PngThrowHelper.ThrowInvalidChunkType("Proprietary Apple PNG detected! This PNG file is not conform to the specification and cannot be decoded."); break; } } finally { chunk.Data?.Dispose(); // Data is rented in ReadChunkData() } } if (image is null) { PngThrowHelper.ThrowNoData(); } return image; } finally { this.scanline?.Dispose(); this.previousScanline?.Dispose(); } } /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; this.currentStream.Skip(8); try { while (!this.isEndChunkReached && this.TryReadChunk(out PngChunk chunk)) { try { switch (chunk.Type) { case PngChunkType.Header: this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.Physical: this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Gamma: this.ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.Data: this.SkipChunkDataAndCrc(chunk); break; case PngChunkType.Text: this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.CompressedText: this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.InternationalText: this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.Exif: if (!this.ignoreMetadata) { var exifData = new byte[chunk.Length]; chunk.Data.GetSpan().CopyTo(exifData); metadata.ExifProfile = new ExifProfile(exifData); } break; case PngChunkType.End: this.isEndChunkReached = true; break; } } finally { chunk.Data?.Dispose(); // Data is rented in ReadChunkData() } } } finally { this.scanline?.Dispose(); this.previousScanline?.Dispose(); } if (this.header.Width == 0 && this.header.Height == 0) { PngThrowHelper.ThrowNoHeader(); } return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); } /// /// Reads the least significant bits from the byte pair with the others set to 0. /// /// The source buffer /// THe offset /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] private static byte ReadByteLittleEndian(ReadOnlySpan buffer, int offset) => (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF)); /// /// Attempts to convert a byte array to a new array where each value in the original array is represented by the /// specified number of bits. /// /// The bytes to convert from. Cannot be empty. /// The number of bytes per scanline /// The number of bits per value. /// The new array. /// The resulting array. private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, out IMemoryOwner buffer) { if (bits >= 8) { buffer = null; return false; } buffer = this.memoryAllocator.Allocate(bytesPerScanline * 8 / bits, AllocationOptions.Clean); ref byte sourceRef = ref MemoryMarshal.GetReference(source); ref byte resultRef = ref buffer.GetReference(); int mask = 0xFF >> (8 - bits); int resultOffset = 0; for (int i = 0; i < bytesPerScanline; i++) { byte b = Unsafe.Add(ref sourceRef, i); for (int shift = 0; shift < 8; shift += bits) { int colorIndex = (b >> (8 - bits - shift)) & mask; Unsafe.Add(ref resultRef, resultOffset) = (byte)colorIndex; resultOffset++; } } return true; } /// /// Reads the data chunk containing physical dimension data. /// /// The metadata to read to. /// The data containing physical data. private void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan data) { var physicalChunk = PhysicalChunkData.Parse(data); metadata.ResolutionUnits = physicalChunk.UnitSpecifier == byte.MinValue ? PixelResolutionUnit.AspectRatio : PixelResolutionUnit.PixelsPerMeter; metadata.HorizontalResolution = physicalChunk.XAxisPixelsPerUnit; metadata.VerticalResolution = physicalChunk.YAxisPixelsPerUnit; } /// /// Reads the data chunk containing gamma data. /// /// The metadata to read to. /// The data containing physical data. private void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan data) { // The value is encoded as a 4-byte unsigned integer, representing gamma times 100000. // For example, a gamma of 1/2.2 would be stored as 45455. pngMetadata.Gamma = BinaryPrimitives.ReadUInt32BigEndian(data) / 100_000F; } /// /// Initializes the image and various buffers needed for processing /// /// The type the pixels will be /// The metadata information for the image /// The image that we will populate private void InitializeImage(ImageMetadata metadata, out Image image) where TPixel : unmanaged, IPixel { image = Image.CreateUninitialized( this.Configuration, this.header.Width, this.header.Height, metadata); this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerSample = 1; if (this.header.BitDepth >= 8) { this.bytesPerSample = this.header.BitDepth / 8; } this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); this.scanline = this.Configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); } /// /// Calculates the correct number of bits per pixel for the given color type. /// /// The private int CalculateBitsPerPixel() { switch (this.pngColorType) { case PngColorType.Grayscale: case PngColorType.Palette: return this.header.BitDepth; case PngColorType.GrayscaleWithAlpha: return this.header.BitDepth * 2; case PngColorType.Rgb: return this.header.BitDepth * 3; case PngColorType.RgbWithAlpha: return this.header.BitDepth * 4; default: PngThrowHelper.ThrowNotSupportedColor(); return -1; } } /// /// Calculates the correct number of bytes per pixel for the given color type. /// /// The private int CalculateBytesPerPixel() { switch (this.pngColorType) { case PngColorType.Grayscale: return this.header.BitDepth == 16 ? 2 : 1; case PngColorType.GrayscaleWithAlpha: return this.header.BitDepth == 16 ? 4 : 2; case PngColorType.Palette: return 1; case PngColorType.Rgb: return this.header.BitDepth == 16 ? 6 : 3; case PngColorType.RgbWithAlpha: default: return this.header.BitDepth == 16 ? 8 : 4; } } /// /// Calculates the scanline length. /// /// The width of the row. /// /// The representing the length. /// private int CalculateScanlineLength(int width) { int mod = this.header.BitDepth == 16 ? 16 : 8; int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel; int amount = scanlineLength % mod; if (amount != 0) { scanlineLength += mod - amount; } return scanlineLength / mod; } /// /// Reads the scanlines within the image. /// /// The pixel format. /// The png chunk containing the compressed scanline data. /// The pixel data. /// The png metadata private void ReadScanlines(PngChunk chunk, ImageFrame image, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { using (var deframeStream = new ZlibInflateStream(this.currentStream, this.ReadNextDataChunk)) { deframeStream.AllocateNewBytes(chunk.Length, true); DeflateStream dataStream = deframeStream.CompressedStream; if (this.header.InterlaceMethod == PngInterlaceMode.Adam7) { this.DecodeInterlacedPixelData(dataStream, image, pngMetadata); } else { this.DecodePixelData(dataStream, image, pngMetadata); } } } /// /// Decodes the raw pixel data row by row /// /// The pixel format. /// The compressed pixel data stream. /// The image to decode to. /// The png metadata private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { while (this.currentRow < this.header.Height) { Span scanlineSpan = this.scanline.GetSpan(); while (this.currentRowBytesRead < this.bytesPerScanline) { int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead); if (bytesRead <= 0) { return; } this.currentRowBytesRead += bytesRead; } this.currentRowBytesRead = 0; switch ((FilterType)scanlineSpan[0]) { case FilterType.None: break; case FilterType.Sub: SubFilter.Decode(scanlineSpan, this.bytesPerPixel); break; case FilterType.Up: UpFilter.Decode(scanlineSpan, this.previousScanline.GetSpan()); break; case FilterType.Average: AverageFilter.Decode(scanlineSpan, this.previousScanline.GetSpan(), this.bytesPerPixel); break; case FilterType.Paeth: PaethFilter.Decode(scanlineSpan, this.previousScanline.GetSpan(), this.bytesPerPixel); break; default: PngThrowHelper.ThrowUnknownFilter(); break; } this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata); this.SwapScanlineBuffers(); this.currentRow++; } } /// /// Decodes the raw interlaced pixel data row by row /// /// /// The pixel format. /// The compressed pixel data stream. /// The current image. /// The png metadata. private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { int pass = 0; int width = this.header.Width; while (true) { int numColumns = Adam7.ComputeColumns(width, pass); if (numColumns == 0) { pass++; // This pass contains no data; skip to next pass continue; } int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; while (this.currentRow < this.header.Height) { while (this.currentRowBytesRead < bytesPerInterlaceScanline) { int bytesRead = compressedStream.Read(this.scanline.GetSpan(), this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead); if (bytesRead <= 0) { return; } this.currentRowBytesRead += bytesRead; } this.currentRowBytesRead = 0; Span scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline); Span prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline); switch ((FilterType)scanSpan[0]) { case FilterType.None: break; case FilterType.Sub: SubFilter.Decode(scanSpan, this.bytesPerPixel); break; case FilterType.Up: UpFilter.Decode(scanSpan, prevSpan); break; case FilterType.Average: AverageFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel); break; case FilterType.Paeth: PaethFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel); break; default: PngThrowHelper.ThrowUnknownFilter(); break; } Span rowSpan = image.GetPixelRowSpan(this.currentRow); this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]); this.SwapScanlineBuffers(); this.currentRow += Adam7.RowIncrement[pass]; } pass++; this.previousScanline.Clear(); if (pass < 7) { this.currentRow = Adam7.FirstRow[pass]; } else { pass = 0; break; } } } /// /// Processes the de-filtered scanline filling the image pixel data /// /// The pixel format. /// The de-filtered scanline /// The image /// The png metadata. private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { Span rowSpan = pixels.GetPixelRowSpan(this.currentRow); // Trim the first marker byte from the buffer ReadOnlySpan trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1); // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. IMemoryOwner buffer = null; try { ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( trimmed, this.bytesPerScanline - 1, this.header.BitDepth, out buffer) ? buffer.GetSpan() : trimmed; switch (this.pngColorType) { case PngColorType.Grayscale: PngScanlineProcessor.ProcessGrayscaleScanline( this.header, scanlineSpan, rowSpan, pngMetadata.HasTransparency, pngMetadata.TransparentL16.GetValueOrDefault(), pngMetadata.TransparentL8.GetValueOrDefault()); break; case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline( this.header, scanlineSpan, rowSpan, this.bytesPerPixel, this.bytesPerSample); break; case PngColorType.Palette: PngScanlineProcessor.ProcessPaletteScanline( this.header, scanlineSpan, rowSpan, this.palette, this.paletteAlpha); break; case PngColorType.Rgb: PngScanlineProcessor.ProcessRgbScanline( this.Configuration, this.header, scanlineSpan, rowSpan, this.bytesPerPixel, this.bytesPerSample, pngMetadata.HasTransparency, pngMetadata.TransparentRgb48.GetValueOrDefault(), pngMetadata.TransparentRgb24.GetValueOrDefault()); break; case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( this.Configuration, this.header, scanlineSpan, rowSpan, this.bytesPerPixel, this.bytesPerSample); break; } } finally { buffer?.Dispose(); } } /// /// Processes the interlaced de-filtered scanline filling the image pixel data /// /// The pixel format. /// The de-filtered scanline /// The current image row. /// The png metadata. /// The column start index. Always 0 for none interlaced images. /// The column increment. Always 1 for none interlaced images. private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defilteredScanline, Span rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1) where TPixel : unmanaged, IPixel { // Trim the first marker byte from the buffer ReadOnlySpan trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1); // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. IMemoryOwner buffer = null; try { ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( trimmed, this.bytesPerScanline, this.header.BitDepth, out buffer) ? buffer.GetSpan() : trimmed; switch (this.pngColorType) { case PngColorType.Grayscale: PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( this.header, scanlineSpan, rowSpan, pixelOffset, increment, pngMetadata.HasTransparency, pngMetadata.TransparentL16.GetValueOrDefault(), pngMetadata.TransparentL8.GetValueOrDefault()); break; case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( this.header, scanlineSpan, rowSpan, pixelOffset, increment, this.bytesPerPixel, this.bytesPerSample); break; case PngColorType.Palette: PngScanlineProcessor.ProcessInterlacedPaletteScanline( this.header, scanlineSpan, rowSpan, pixelOffset, increment, this.palette, this.paletteAlpha); break; case PngColorType.Rgb: PngScanlineProcessor.ProcessInterlacedRgbScanline( this.header, scanlineSpan, rowSpan, pixelOffset, increment, this.bytesPerPixel, this.bytesPerSample, pngMetadata.HasTransparency, pngMetadata.TransparentRgb48.GetValueOrDefault(), pngMetadata.TransparentRgb24.GetValueOrDefault()); break; case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessInterlacedRgbaScanline( this.header, scanlineSpan, rowSpan, pixelOffset, increment, this.bytesPerPixel, this.bytesPerSample); break; } } finally { buffer?.Dispose(); } } /// /// Decodes and assigns marker colors that identify transparent pixels in non indexed images. /// /// The alpha tRNS array. /// The png metadata. private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngMetadata) { if (this.pngColorType == PngColorType.Rgb) { if (alpha.Length >= 6) { if (this.header.BitDepth == 16) { ushort rc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2)); ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); pngMetadata.TransparentRgb48 = new Rgb48(rc, gc, bc); pngMetadata.HasTransparency = true; return; } byte r = ReadByteLittleEndian(alpha, 0); byte g = ReadByteLittleEndian(alpha, 2); byte b = ReadByteLittleEndian(alpha, 4); pngMetadata.TransparentRgb24 = new Rgb24(r, g, b); pngMetadata.HasTransparency = true; } } else if (this.pngColorType == PngColorType.Grayscale) { if (alpha.Length >= 2) { if (this.header.BitDepth == 16) { pngMetadata.TransparentL16 = new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2))); } else { pngMetadata.TransparentL8 = new L8(ReadByteLittleEndian(alpha, 0)); } pngMetadata.HasTransparency = true; } } } /// /// Reads a header chunk from the data. /// /// The png metadata. /// The containing data. private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data) { this.header = PngHeader.Parse(data); this.header.Validate(); pngMetadata.BitDepth = (PngBitDepth)this.header.BitDepth; pngMetadata.ColorType = this.header.ColorType; pngMetadata.InterlaceMethod = this.header.InterlaceMethod; this.pngColorType = this.header.ColorType; } /// /// Reads a text chunk containing image properties from the data. /// /// The metadata to decode to. /// The containing the data. private void ReadTextChunk(PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { return; } int zeroIndex = data.IndexOf((byte)0); // Keywords are restricted to 1 to 79 bytes in length. if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength) { return; } ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); if (!this.TryReadTextKeyword(keywordBytes, out string name)) { return; } string value = PngConstants.Encoding.GetString(data.Slice(zeroIndex + 1)); metadata.TextData.Add(new PngTextData(name, value, string.Empty, string.Empty)); } /// /// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string. /// /// The metadata to decode to. /// The containing the data. private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { return; } int zeroIndex = data.IndexOf((byte)0); if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength) { return; } byte compressionMethod = data[zeroIndex + 1]; if (compressionMethod != 0) { // Only compression method 0 is supported (zlib datastream with deflate compression). return; } ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); if (!this.TryReadTextKeyword(keywordBytes, out string name)) { return; } ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string uncompressed)) { metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty)); } } /// /// Reads a iTXt chunk, which contains international text data. It contains: /// - A uncompressed keyword. /// - Compression flag, indicating if a compression is used. /// - Compression method. /// - Language tag (optional). /// - A translated keyword (optional). /// - Text data, which is either compressed or uncompressed. /// /// The metadata to decode to. /// The containing the data. private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { return; } int zeroIndexKeyword = data.IndexOf((byte)0); if (zeroIndexKeyword < PngConstants.MinTextKeywordLength || zeroIndexKeyword > PngConstants.MaxTextKeywordLength) { return; } byte compressionFlag = data[zeroIndexKeyword + 1]; if (!(compressionFlag == 0 || compressionFlag == 1)) { return; } byte compressionMethod = data[zeroIndexKeyword + 2]; if (compressionMethod != 0) { // Only compression method 0 is supported (zlib datastream with deflate compression). return; } int langStartIdx = zeroIndexKeyword + 3; int languageLength = data.Slice(langStartIdx).IndexOf((byte)0); if (languageLength < 0) { return; } string language = PngConstants.LanguageEncoding.GetString(data.Slice(langStartIdx, languageLength)); int translatedKeywordStartIdx = langStartIdx + languageLength + 1; int translatedKeywordLength = data.Slice(translatedKeywordStartIdx).IndexOf((byte)0); string translatedKeyword = PngConstants.TranslatedEncoding.GetString(data.Slice(translatedKeywordStartIdx, translatedKeywordLength)); ReadOnlySpan keywordBytes = data.Slice(0, zeroIndexKeyword); if (!this.TryReadTextKeyword(keywordBytes, out string keyword)) { return; } int dataStartIdx = translatedKeywordStartIdx + translatedKeywordLength + 1; if (compressionFlag == 1) { ReadOnlySpan compressedData = data.Slice(dataStartIdx); if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string uncompressed)) { metadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword)); } } else { string value = PngConstants.TranslatedEncoding.GetString(data.Slice(dataStartIdx)); metadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword)); } } /// /// Decompresses a byte array with zlib compressed text data. /// /// Compressed text data bytes. /// The string encoding to use. /// The uncompressed value. /// The . private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value) { using (var memoryStream = new MemoryStream(compressedData.ToArray())) using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) using (var inflateStream = new ZlibInflateStream(bufferedStream)) { if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) { value = null; return false; } var uncompressedBytes = new List(); // Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here. int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); while (bytesRead != 0) { uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray()); bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); } value = encoding.GetString(uncompressedBytes.ToArray()); return true; } } /// /// Reads the next data chunk. /// /// Count of bytes in the next data chunk, or 0 if there are no more data chunks left. private int ReadNextDataChunk() { if (this.nextChunk != null) { return 0; } this.currentStream.Read(this.buffer, 0, 4); if (this.TryReadChunk(out PngChunk chunk)) { if (chunk.Type == PngChunkType.Data) { return chunk.Length; } this.nextChunk = chunk; } return 0; } /// /// Reads a chunk from the stream. /// /// The image format chunk. /// /// The . /// private bool TryReadChunk(out PngChunk chunk) { if (this.nextChunk != null) { chunk = this.nextChunk.Value; this.nextChunk = null; return true; } if (!this.TryReadChunkLength(out int length)) { chunk = default; // IEND return false; } while (length < 0 || length > (this.currentStream.Length - this.currentStream.Position)) { // Not a valid chunk so try again until we reach a known chunk. if (!this.TryReadChunkLength(out length)) { chunk = default; return false; } } PngChunkType type = this.ReadChunkType(); // NOTE: Reading the chunk data is the responsible of the caller if (type == PngChunkType.Data) { chunk = new PngChunk(length, type); return true; } chunk = new PngChunk( length: length, type: type, data: this.ReadChunkData(length)); this.ValidateChunk(chunk); return true; } /// /// Validates the png chunk. /// /// The . private void ValidateChunk(in PngChunk chunk) { uint inputCrc = this.ReadChunkCrc(); if (chunk.IsCritical) { Span chunkType = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); uint validCrc = Crc32.Calculate(chunkType); validCrc = Crc32.Calculate(validCrc, chunk.Data.GetSpan()); if (validCrc != inputCrc) { string chunkTypeName = Encoding.ASCII.GetString(chunkType); PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName); } } } /// /// Reads the cycle redundancy chunk from the data. /// [MethodImpl(InliningOptions.ShortMethod)] private uint ReadChunkCrc() { uint crc = 0; if (this.currentStream.Read(this.buffer, 0, 4) == 4) { crc = BinaryPrimitives.ReadUInt32BigEndian(this.buffer); } return crc; } /// /// Skips the chunk data and the cycle redundancy chunk read from the data. /// /// The image format chunk. [MethodImpl(InliningOptions.ShortMethod)] private void SkipChunkDataAndCrc(in PngChunk chunk) { this.currentStream.Skip(chunk.Length); this.currentStream.Skip(4); } /// /// Reads the chunk data from the stream. /// /// The length of the chunk data to read. [MethodImpl(InliningOptions.ShortMethod)] private IMemoryOwner ReadChunkData(int length) { // We rent the buffer here to return it afterwards in Decode() IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean); this.currentStream.Read(buffer.GetSpan(), 0, length); return buffer; } /// /// Identifies the chunk type from the chunk. /// /// /// Thrown if the input stream is not valid. /// [MethodImpl(InliningOptions.ShortMethod)] private PngChunkType ReadChunkType() { if (this.currentStream.Read(this.buffer, 0, 4) == 4) { return (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer); } else { PngThrowHelper.ThrowInvalidChunkType(); // The IDE cannot detect the throw here. return default; } } /// /// Attempts to read the length of the next chunk. /// /// /// Whether the length was read. /// [MethodImpl(InliningOptions.ShortMethod)] private bool TryReadChunkLength(out int result) { if (this.currentStream.Read(this.buffer, 0, 4) == 4) { result = BinaryPrimitives.ReadInt32BigEndian(this.buffer); return true; } result = default; return false; } /// /// Tries to reads a text chunk keyword, which have some restrictions to be valid: /// Keywords shall contain only printable Latin-1 characters and should not have leading or trailing whitespace. /// See: https://www.w3.org/TR/PNG/#11zTXt /// /// The keyword bytes. /// The name. /// True, if the keyword could be read and is valid. private bool TryReadTextKeyword(ReadOnlySpan keywordBytes, out string name) { name = string.Empty; // Keywords shall contain only printable Latin-1. foreach (byte c in keywordBytes) { if (!((c >= 32 && c <= 126) || (c >= 161 && c <= 255))) { return false; } } // Keywords should not be empty or have leading or trailing whitespace. name = PngConstants.Encoding.GetString(keywordBytes); if (string.IsNullOrWhiteSpace(name) || name.StartsWith(" ") || name.EndsWith(" ")) { return false; } return true; } private void SwapScanlineBuffers() { IMemoryOwner temp = this.previousScanline; this.previousScanline = this.scanline; this.scanline = temp; } } }