diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index f3231fa22..77bc9f7a0 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -14,12 +14,12 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. /// - PngBitDepth BitDepth { get; } + PngBitDepth? BitDepth { get; } /// /// Gets the color type /// - PngColorType ColorType { get; } + PngColorType? ColorType { get; } /// /// Gets the filter method. @@ -33,15 +33,13 @@ namespace SixLabors.ImageSharp.Formats.Png int CompressionLevel { get; } /// - /// Gets the gamma value, that will be written - /// the the stream, when the property - /// is set to true. The default value is 2.2F. + /// Gets the gamma value, that will be written the the image. /// /// The gamma value of the image. - float Gamma { get; } + float? Gamma { get; } /// - /// Gets quantizer for reducing the color count. + /// Gets the quantizer for reducing the color count. /// IQuantizer Quantizer { get; } @@ -49,11 +47,5 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets the transparency threshold. /// byte Threshold { get; } - - /// - /// Gets a value indicating whether this instance should write - /// gamma information to the stream. The default value is false. - /// - bool WriteGamma { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index e0844ca6b..7654c1701 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -8,58 +8,58 @@ namespace SixLabors.ImageSharp.Formats.Png /// internal enum PngChunkType : uint { - /// - /// The first chunk in a png file. Can only exists once. Contains - /// common information like the width and the height of the image or - /// the used compression method. - /// - Header = 0x49484452U, // IHDR - - /// - /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte - /// series in the RGB format. - /// - Palette = 0x504C5445U, // PLTE - /// /// The IDAT chunk contains the actual image data. The image can contains more /// than one chunk of this type. All chunks together are the whole image. /// - Data = 0x49444154U, // IDAT + Data = 0x49444154U, /// /// This chunk must appear last. It marks the end of the PNG data stream. /// The chunk's data field is empty. /// - End = 0x49454E44U, // IEND + End = 0x49454E44U, /// - /// This chunk specifies that the image uses simple transparency: - /// either alpha values associated with palette entries (for indexed-color images) - /// or a single transparent color (for grayscale and true color images). + /// The first chunk in a png file. Can only exists once. Contains + /// common information like the width and the height of the image or + /// the used compression method. /// - PaletteAlpha = 0x74524E53U, // tRNS + Header = 0x49484452U, /// - /// Textual information that the encoder wishes to record with the image can be stored in - /// tEXt chunks. Each tEXt chunk contains a keyword and a text string. + /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte + /// series in the RGB format. + /// + Palette = 0x504C5445U, + + /// + /// The eXIf data chunk which contains the Exif profile. /// - Text = 0x74455874U, // tEXt + Exif = 0x65584966U, /// /// This chunk specifies the relationship between the image samples and the desired /// display output intensity. /// - Gamma = 0x67414D41U, // gAMA + Gamma = 0x67414D41U, /// /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// - Physical = 0x70485973U, // pHYs + Physical = 0x70485973U, /// - /// The data chunk which contains the Exif profile. + /// Textual information that the encoder wishes to record with the image can be stored in + /// tEXt chunks. Each tEXt chunk contains a keyword and a text string. + /// + Text = 0x74455874U, + + /// + /// This chunk specifies that the image uses simple transparency: + /// either alpha values associated with palette entries (for indexed-color images) + /// or a single transparent color (for grayscale and true color images). /// - Exif = 0x65584966U // eXIf + PaletteAlpha = 0x74524E53U } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index be1914174..4bc483301 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -216,7 +216,9 @@ namespace SixLabors.ImageSharp.Formats.Png public Image Decode(Stream stream) where TPixel : struct, IPixel { - var metadata = new ImageMetaData(); + var metaData = new ImageMetaData(); + var pngMetaData = new PngMetaData(); + metaData.AddOrUpdateFormatMetaData(PngFormat.Instance, pngMetaData); this.currentStream = stream; this.currentStream.Skip(8); Image image = null; @@ -231,16 +233,19 @@ namespace SixLabors.ImageSharp.Formats.Png switch (chunk.Type) { case PngChunkType.Header: - this.ReadHeaderChunk(chunk.Data.Array); + this.ReadHeaderChunk(pngMetaData, chunk.Data.Array); this.ValidateHeader(); break; case PngChunkType.Physical: - this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); + 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.InitializeImage(metaData, out image); } deframeStream.AllocateNewBytes(chunk.Length); @@ -259,14 +264,14 @@ namespace SixLabors.ImageSharp.Formats.Png this.AssignTransparentMarkers(alpha); break; case PngChunkType.Text: - this.ReadTextChunk(metadata, chunk.Data.Array, chunk.Length); + this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length); break; case PngChunkType.Exif: if (!this.ignoreMetadata) { byte[] exifData = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length); - metadata.ExifProfile = new ExifProfile(exifData); + metaData.ExifProfile = new ExifProfile(exifData); } break; @@ -302,7 +307,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// The containing image data. public IImageInfo Identify(Stream stream) { - var metadata = new ImageMetaData(); + var metaData = new ImageMetaData(); + var pngMetaData = new PngMetaData(); + metaData.AddOrUpdateFormatMetaData(PngFormat.Instance, pngMetaData); this.currentStream = stream; this.currentStream.Skip(8); try @@ -314,17 +321,20 @@ namespace SixLabors.ImageSharp.Formats.Png switch (chunk.Type) { case PngChunkType.Header: - this.ReadHeaderChunk(chunk.Data.Array); + this.ReadHeaderChunk(pngMetaData, chunk.Data.Array); this.ValidateHeader(); break; case PngChunkType.Physical: - this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); + 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(metadata, chunk.Data.Array, chunk.Length); + this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length); break; case PngChunkType.End: this.isEndChunkReached = true; @@ -348,7 +358,7 @@ namespace SixLabors.ImageSharp.Formats.Png throw new ImageFormatException("PNG Image does not contain a header chunk"); } - return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metaData); } /// @@ -427,6 +437,18 @@ namespace SixLabors.ImageSharp.Formats.Png metadata.VerticalResolution = vResolution; } + /// + /// 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 /// @@ -1267,17 +1289,27 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Reads a header chunk from the data. /// + /// The png metadata. /// The containing data. - private void ReadHeaderChunk(ReadOnlySpan data) + private void ReadHeaderChunk(PngMetaData pngMetaData, ReadOnlySpan data) { + byte bitDepth = data[8]; this.header = new PngHeader( width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)), height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)), - bitDepth: data[8], + bitDepth: bitDepth, colorType: (PngColorType)data[9], compressionMethod: data[10], filterMethod: data[11], interlaceMethod: (PngInterlaceMode)data[12]); + + // TODO: Figure out how we can determine the number of colors and support more bit depths. + if (bitDepth == 8 || bitDepth == 16) + { + pngMetaData.BitDepth = (PngBitDepth)bitDepth; + } + + pngMetaData.ColorType = this.header.ColorType; } /// diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 109e6ad77..435d0abbc 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -4,6 +4,7 @@ using System.IO; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png @@ -17,12 +18,12 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets or sets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. /// - public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8; + public PngBitDepth? BitDepth { get; set; } /// /// Gets or sets the color type. /// - public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha; + public PngColorType? ColorType { get; set; } /// /// Gets or sets the filter method. @@ -36,18 +37,15 @@ namespace SixLabors.ImageSharp.Formats.Png public int CompressionLevel { get; set; } = 6; /// - /// Gets or sets the gamma value, that will be written - /// the the stream, when the property - /// is set to true. The default value is 2.2F. + /// Gets or sets the gamma value, that will be written the the image. /// - /// The gamma value of the image. - public float Gamma { get; set; } = 2.2F; + public float? Gamma { get; set; } /// /// Gets or sets quantizer for reducing the color count. /// Defaults to the /// - public IQuantizer Quantizer { get; set; } = new WuQuantizer(); + public IQuantizer Quantizer { get; set; } = KnownQuantizers.Wu; /// /// Gets or sets the transparency threshold. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ffe29aeca..9d9de71b1 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -4,7 +4,6 @@ using System; using System.Buffers.Binary; using System.IO; -using System.Linq; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Png.Filters; @@ -45,49 +44,49 @@ namespace SixLabors.ImageSharp.Formats.Png private readonly Crc32 crc = new Crc32(); /// - /// The png bit depth + /// The png filter method. /// - private readonly PngBitDepth pngBitDepth; + private readonly PngFilterMethod pngFilterMethod; /// - /// Gets or sets a value indicating whether to use 16 bit encoding for supported color types. + /// The quantizer for reducing the color count. /// - private readonly bool use16Bit; + private readonly IQuantizer quantizer; /// - /// The png color type. + /// Gets or sets the CompressionLevel value /// - private readonly PngColorType pngColorType; + private readonly int compressionLevel; /// - /// The png filter method. + /// Gets or sets the alpha threshold value /// - private readonly PngFilterMethod pngFilterMethod; + private readonly byte threshold; /// - /// The quantizer for reducing the color count. + /// Gets or sets a value indicating whether to write the gamma chunk /// - private readonly IQuantizer quantizer; + private bool writeGamma; /// - /// Gets or sets the CompressionLevel value + /// The png bit depth /// - private readonly int compressionLevel; + private PngBitDepth? pngBitDepth; /// - /// Gets or sets the Gamma value + /// Gets or sets a value indicating whether to use 16 bit encoding for supported color types. /// - private readonly float gamma; + private bool use16Bit; /// - /// Gets or sets the Threshold value + /// The png color type. /// - private readonly byte threshold; + private PngColorType? pngColorType; /// - /// Gets or sets a value indicating whether to Write Gamma + /// Gets or sets the Gamma value /// - private readonly bool writeGamma; + private float? gamma; /// /// The image width. @@ -158,14 +157,12 @@ namespace SixLabors.ImageSharp.Formats.Png { this.memoryAllocator = memoryAllocator; this.pngBitDepth = options.BitDepth; - this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16); this.pngColorType = options.ColorType; this.pngFilterMethod = options.FilterMethod; this.compressionLevel = options.CompressionLevel; this.gamma = options.Gamma; this.quantizer = options.Quantizer; this.threshold = options.Threshold; - this.writeGamma = options.WriteGamma; } /// @@ -183,6 +180,16 @@ namespace SixLabors.ImageSharp.Formats.Png this.width = image.Width; this.height = image.Height; + // Always take the encoder options over the metadata values. + PngMetaData pngMetaData = image.MetaData.GetOrAddFormatMetaData(PngFormat.Instance); + this.gamma = this.gamma ?? pngMetaData.Gamma; + this.writeGamma = this.gamma > 0; + this.pngColorType = this.pngColorType ?? pngMetaData.ColorType; + + // TODO: We don't take full advantage of this information yet. + this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth; + this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16); + stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); QuantizedFrame quantized = null; @@ -217,7 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Png width: image.Width, height: image.Height, bitDepth: this.bitDepth, - colorType: this.pngColorType, + colorType: this.pngColorType.Value, compressionMethod: 0, // None filterMethod: 0, interlaceMethod: 0); // TODO: Can't write interlaced yet. @@ -781,10 +788,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// Writes the chunk end to the stream. /// /// The containing image data. - private void WriteEndChunk(Stream stream) - { - this.WriteChunk(stream, PngChunkType.End, null); - } + private void WriteEndChunk(Stream stream) => this.WriteChunk(stream, PngChunkType.End, null); /// /// Writes a chunk to the stream. @@ -792,10 +796,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The to write to. /// The type of chunk to write. /// The containing data. - private void WriteChunk(Stream stream, PngChunkType type, byte[] data) - { - this.WriteChunk(stream, type, data, 0, data?.Length ?? 0); - } + private void WriteChunk(Stream stream, PngChunkType type, byte[] data) => this.WriteChunk(stream, type, data, 0, data?.Length ?? 0); /// /// Writes a chunk of a specified length to the stream at the given offset. diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index 90dbb83b6..1eb3cdad6 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -8,6 +8,20 @@ namespace SixLabors.ImageSharp.Formats.Png /// public class PngMetaData { - // TODO: Analyse what properties we would like to preserve. + /// + /// Gets or sets the number of bits per sample or per palette index (not per pixel). + /// Not all values are allowed for all values. + /// + public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8; + + /// + /// Gets or sets the color type. + /// + public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha; + + /// + /// Gets or sets the gamma value for the image. + /// + public float Gamma { get; set; } } -} +} \ No newline at end of file