/// <summary> /// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// </summary> /// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param> /// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param> public void Encode <TPixel>(Image <TPixel> image, Stream stream) where TPixel : struct, IPixel <TPixel> { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); this.width = image.Width; this.height = image.Height; ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetPngMetadata(); PngEncoderOptionsHelpers.AdjustOptions <TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); IQuantizedFrame <TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized); stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); this.WriteHeaderChunk(stream); this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); this.WriteGammaChunk(stream); this.WriteExifChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); this.WriteEndChunk(stream); stream.Flush(); quantized?.Dispose(); }
/// <summary> /// Adjusts the options based upon the given metadata. /// </summary> /// <param name="options">The options.</param> /// <param name="pngMetadata">The PNG metadata.</param> /// <param name="use16Bit">if set to <c>true</c> [use16 bit].</param> /// <param name="bytesPerPixel">The bytes per pixel.</param> public static void AdjustOptions <TPixel>( PngEncoderOptions options, PngMetadata pngMetadata, out bool use16Bit, out int bytesPerPixel) where TPixel : unmanaged, IPixel <TPixel> { // Always take the encoder options over the metadata values. options.Gamma ??= pngMetadata.Gamma; // Use options, then check metadata, if nothing set there then we suggest // a sensible default based upon the pixel format. options.ColorType ??= pngMetadata.ColorType ?? SuggestColorType <TPixel>(); options.BitDepth ??= pngMetadata.BitDepth ?? SuggestBitDepth <TPixel>(); // Ensure bit depth and color type are a supported combination. // Bit8 is the only bit depth supported by all color types. byte bits = (byte)options.BitDepth; byte[] validBitDepths = PngConstants.ColorTypes[options.ColorType.Value]; if (Array.IndexOf(validBitDepths, bits) == -1) { options.BitDepth = PngBitDepth.Bit8; } options.InterlaceMethod ??= pngMetadata.InterlaceMethod; use16Bit = options.BitDepth == PngBitDepth.Bit16; bytesPerPixel = CalculateBytesPerPixel(options.ColorType, use16Bit); if (options.IgnoreMetadata) { options.ChunkFilter = PngChunkFilter.ExcludeAll; } }
/// <summary> /// Adjusts the options based upon the given metadata. /// </summary> /// <param name="options">The options.</param> /// <param name="pngMetadata">The PNG metadata.</param> /// <param name="use16Bit">if set to <c>true</c> [use16 bit].</param> /// <param name="bytesPerPixel">The bytes per pixel.</param> public static void AdjustOptions <TPixel>( PngEncoderOptions options, PngMetadata pngMetadata, out bool use16Bit, out int bytesPerPixel) where TPixel : unmanaged, IPixel <TPixel> { // Always take the encoder options over the metadata values. options.Gamma ??= pngMetadata.Gamma; // Use options, then check metadata, if nothing set there then we suggest // a sensible default based upon the pixel format. options.ColorType ??= pngMetadata.ColorType ?? SuggestColorType <TPixel>(); options.BitDepth ??= pngMetadata.BitDepth ?? SuggestBitDepth <TPixel>(); options.InterlaceMethod ??= pngMetadata.InterlaceMethod; use16Bit = options.BitDepth == PngBitDepth.Bit16; bytesPerPixel = CalculateBytesPerPixel(options.ColorType, use16Bit); if (options.IgnoreMetadata) { options.ChunkFilter = PngChunkFilter.ExcludeAll; } // Ensure we are not allowing impossible combinations. if (!PngConstants.ColorTypes.ContainsKey(options.ColorType.Value)) { throw new NotSupportedException("Color type is not supported or not valid."); } }
/// <summary> /// Reads the raw image information from the specified stream. /// </summary> /// <param name="stream">The <see cref="Stream"/> containing image data.</param> public IImageInfo Identify(Stream stream) { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); 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.Array); 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.Array.AsSpan(0, chunk.Length)); 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)); }
/// <summary> /// Initializes a new instance of the <see cref="PngMetadata"/> class. /// </summary> /// <param name="other">The metadata to create an instance from.</param> private PngMetadata(PngMetadata other) { this.BitDepth = other.BitDepth; this.ColorType = other.ColorType; this.Gamma = other.Gamma; this.HasTrans = other.HasTrans; this.TransparentGray8 = other.TransparentGray8; this.TransparentGray16 = other.TransparentGray16; this.TransparentRgb24 = other.TransparentRgb24; this.TransparentRgb48 = other.TransparentRgb48; }
/// <inheritdoc /> public async Task <Image> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { PngDecoderCore decoder = new(configuration, true); IImageInfo info = await decoder.IdentifyAsync(configuration, stream, cancellationToken).ConfigureAwait(false); stream.Position = 0; PngMetadata meta = info.Metadata.GetPngMetadata(); PngColorType color = meta.ColorType.GetValueOrDefault(); PngBitDepth bits = meta.BitDepth.GetValueOrDefault(); switch (color) { case PngColorType.Grayscale: if (bits == PngBitDepth.Bit16) { return(!meta.HasTransparency ? await this.DecodeAsync <L16>(configuration, stream, cancellationToken).ConfigureAwait(false) : await this.DecodeAsync <La32>(configuration, stream, cancellationToken).ConfigureAwait(false)); } return(!meta.HasTransparency ? await this.DecodeAsync <L8>(configuration, stream, cancellationToken).ConfigureAwait(false) : await this.DecodeAsync <La16>(configuration, stream, cancellationToken).ConfigureAwait(false)); case PngColorType.Rgb: if (bits == PngBitDepth.Bit16) { return(!meta.HasTransparency ? await this.DecodeAsync <Rgb48>(configuration, stream, cancellationToken).ConfigureAwait(false) : await this.DecodeAsync <Rgba64>(configuration, stream, cancellationToken).ConfigureAwait(false)); } return(!meta.HasTransparency ? await this.DecodeAsync <Rgb24>(configuration, stream, cancellationToken).ConfigureAwait(false) : await this.DecodeAsync <Rgba32>(configuration, stream, cancellationToken).ConfigureAwait(false)); case PngColorType.Palette: return(await this.DecodeAsync <Rgba32>(configuration, stream, cancellationToken).ConfigureAwait(false)); case PngColorType.GrayscaleWithAlpha: return((bits == PngBitDepth.Bit16) ? await this.DecodeAsync <La32>(configuration, stream, cancellationToken).ConfigureAwait(false) : await this.DecodeAsync <La16>(configuration, stream, cancellationToken).ConfigureAwait(false)); case PngColorType.RgbWithAlpha: return((bits == PngBitDepth.Bit16) ? await this.DecodeAsync <Rgba64>(configuration, stream, cancellationToken).ConfigureAwait(false) : await this.DecodeAsync <Rgba32>(configuration, stream, cancellationToken).ConfigureAwait(false)); default: return(await this.DecodeAsync <Rgba32>(configuration, stream, cancellationToken).ConfigureAwait(false)); } }
/// <inheritdoc /> public Image Decode(Configuration configuration, Stream stream) { PngDecoderCore decoder = new(configuration, true); IImageInfo info = decoder.Identify(configuration, stream); stream.Position = 0; PngMetadata meta = info.Metadata.GetPngMetadata(); PngColorType color = meta.ColorType.GetValueOrDefault(); PngBitDepth bits = meta.BitDepth.GetValueOrDefault(); switch (color) { case PngColorType.Grayscale: if (bits == PngBitDepth.Bit16) { return(!meta.HasTransparency ? this.Decode <L16>(configuration, stream) : this.Decode <La32>(configuration, stream)); } return(!meta.HasTransparency ? this.Decode <L8>(configuration, stream) : this.Decode <La16>(configuration, stream)); case PngColorType.Rgb: if (bits == PngBitDepth.Bit16) { return(!meta.HasTransparency ? this.Decode <Rgb48>(configuration, stream) : this.Decode <Rgba64>(configuration, stream)); } return(!meta.HasTransparency ? this.Decode <Rgb24>(configuration, stream) : this.Decode <Rgba32>(configuration, stream)); case PngColorType.Palette: return(this.Decode <Rgba32>(configuration, stream)); case PngColorType.GrayscaleWithAlpha: return((bits == PngBitDepth.Bit16) ? this.Decode <La32>(configuration, stream) : this.Decode <La16>(configuration, stream)); case PngColorType.RgbWithAlpha: return((bits == PngBitDepth.Bit16) ? this.Decode <Rgba64>(configuration, stream) : this.Decode <Rgba32>(configuration, stream)); default: return(this.Decode <Rgba32>(configuration, stream)); } }
/// <summary> /// Initializes a new instance of the <see cref="PngMetadata"/> class. /// </summary> /// <param name="other">The metadata to create an instance from.</param> private PngMetadata(PngMetadata other) { this.BitDepth = other.BitDepth; this.ColorType = other.ColorType; this.Gamma = other.Gamma; this.InterlaceMethod = other.InterlaceMethod; this.HasTransparency = other.HasTransparency; this.TransparentL8 = other.TransparentL8; this.TransparentL16 = other.TransparentL16; this.TransparentRgb24 = other.TransparentRgb24; this.TransparentRgb48 = other.TransparentRgb48; for (int i = 0; i < other.TextData.Count; i++) { this.TextData.Add(other.TextData[i]); } }
/// <summary> /// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// </summary> /// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param> /// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param> public void Encode <TPixel>(Image <TPixel> image, Stream stream) where TPixel : unmanaged, IPixel <TPixel> { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); this.width = image.Width; this.height = image.Height; ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngEncoderOptionsHelpers.AdjustOptions <TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); Image <TPixel> clonedImage = null; bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear; if (clearTransparency) { clonedImage = image.Clone(); ClearTransparentPixels(clonedImage); } IndexedImageFrame <TPixel> quantized = this.CreateQuantizedImage(image, clonedImage); stream.Write(PngConstants.HeaderBytes); this.WriteHeaderChunk(stream); this.WriteGammaChunk(stream); this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); this.WriteExifChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream); this.WriteEndChunk(stream); stream.Flush(); quantized?.Dispose(); clonedImage?.Dispose(); }
/// <summary> /// Adjusts the options. /// </summary> /// <param name="options">The options.</param> /// <param name="pngMetadata">The PNG metadata.</param> /// <param name="use16Bit">if set to <c>true</c> [use16 bit].</param> /// <param name="bytesPerPixel">The bytes per pixel.</param> public static void AdjustOptions( PngEncoderOptions options, PngMetadata pngMetadata, out bool use16Bit, out int bytesPerPixel) { // Always take the encoder options over the metadata values. options.Gamma = options.Gamma ?? pngMetadata.Gamma; options.ColorType = options.ColorType ?? pngMetadata.ColorType; options.BitDepth = options.BitDepth ?? pngMetadata.BitDepth; options.InterlaceMethod = options.InterlaceMethod ?? pngMetadata.InterlaceMethod; use16Bit = options.BitDepth == PngBitDepth.Bit16; bytesPerPixel = CalculateBytesPerPixel(options.ColorType, use16Bit); // Ensure we are not allowing impossible combinations. if (!PngConstants.ColorTypes.ContainsKey(options.ColorType.Value)) { throw new NotSupportedException("Color type is not supported or not valid."); } }
/// <summary> /// Decodes the stream to the image. /// </summary> /// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="stream">The stream containing image data. </param> /// <exception cref="ImageFormatException"> /// Thrown if the stream does not contain and end chunk. /// </exception> /// <exception cref="ArgumentOutOfRangeException"> /// Thrown if the image is larger than the maximum allowable size. /// </exception> /// <returns>The decoded image</returns> public Image <TPixel> Decode <TPixel>(Stream stream) where TPixel : struct, IPixel <TPixel> { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); this.currentStream = stream; this.currentStream.Skip(8); Image <TPixel> image = null; try { while (!this.isEndChunkReached && this.TryReadChunk(out PngChunk chunk)) { try { switch (chunk.Type) { case PngChunkType.Header: this.ReadHeaderChunk(pngMetadata, chunk.Data.Array); 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]; Buffer.BlockCopy(chunk.Data.Array, 0, pal, 0, chunk.Length); this.palette = pal; break; case PngChunkType.Transparency: var alpha = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, alpha, 0, chunk.Length); this.paletteAlpha = alpha; this.AssignTransparentMarkers(alpha, pngMetadata); break; case PngChunkType.Text: this.ReadTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.CompressedText: this.ReadCompressedTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.InternationalText: this.ReadInternationalTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.Exif: if (!this.ignoreMetadata) { var exifData = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length); metadata.ExifProfile = new ExifProfile(exifData); } break; case PngChunkType.End: this.isEndChunkReached = true; 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(); } }
/// <summary> /// Decodes the stream to the image. /// </summary> /// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="stream">The stream containing image data. </param> /// <exception cref="ImageFormatException"> /// Thrown if the stream does not contain and end chunk. /// </exception> /// <exception cref="ArgumentOutOfRangeException"> /// Thrown if the image is larger than the maximum allowable size. /// </exception> /// <returns>The decoded image</returns> public Image <TPixel> Decode <TPixel>(Stream stream) where TPixel : struct, IPixel <TPixel> { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); this.currentStream = stream; this.currentStream.Skip(8); Image <TPixel> image = null; try { while (!this.isEndChunkReached && this.TryReadChunk(out PngChunk chunk)) { try { switch (chunk.Type) { case PngChunkType.Header: this.ReadHeaderChunk(pngMetadata, chunk.Data.Array); 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); } using (var deframeStream = new ZlibInflateStream(this.currentStream, this.ReadNextDataChunk)) { deframeStream.AllocateNewBytes(chunk.Length); this.ReadScanlines(deframeStream.CompressedStream, image.Frames.RootFrame, pngMetadata); } break; case PngChunkType.Palette: byte[] pal = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, pal, 0, chunk.Length); this.palette = pal; break; case PngChunkType.Transparency: byte[] alpha = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, alpha, 0, chunk.Length); this.paletteAlpha = alpha; this.AssignTransparentMarkers(alpha, pngMetadata); break; case PngChunkType.Text: this.ReadTextChunk(metadata, chunk.Data.Array.AsSpan(0, 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); } break; case PngChunkType.End: this.isEndChunkReached = true; break; } } finally { chunk.Data?.Dispose(); // Data is rented in ReadChunkData() } } if (image is null) { throw new ImageFormatException("PNG Image does not contain a data chunk"); } return(image); } finally { this.scanline?.Dispose(); this.previousScanline?.Dispose(); } }
/// <inheritdoc/> public IImageInfo Identify(BufferedReadStream stream) { 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.Array); 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.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.CompressedText: this.ReadCompressedTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.InternationalText: this.ReadInternationalTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.Exif: if (!this.ignoreMetadata) { var exifData = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length); 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)); }
/// <summary> /// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// </summary> /// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param> /// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param> public void Encode <TPixel>(Image <TPixel> image, Stream stream) where TPixel : struct, IPixel <TPixel> { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); this.configuration = image.GetConfiguration(); this.width = image.Width; this.height = image.Height; // Always take the encoder options over the metadata values. ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); this.gamma = this.gamma ?? pngMetadata.Gamma; this.writeGamma = this.gamma > 0; this.pngColorType = this.pngColorType ?? pngMetadata.ColorType; this.pngBitDepth = this.pngBitDepth ?? pngMetadata.BitDepth; this.use16Bit = this.pngBitDepth == PngBitDepth.Bit16; // Ensure we are not allowing impossible combinations. if (!ColorTypes.ContainsKey(this.pngColorType.Value)) { throw new NotSupportedException("Color type is not supported or not valid."); } stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); QuantizedFrame <TPixel> quantized = null; if (this.pngColorType == PngColorType.Palette) { byte bits = (byte)this.pngBitDepth; if (!ColorTypes[this.pngColorType.Value].Contains(bits)) { throw new NotSupportedException("Bit depth is not supported or not valid."); } // Use the metadata to determine what quantization depth to use if no quantizer has been set. if (this.quantizer is null) { this.quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits)); } // Create quantized frame returning the palette and set the bit depth. quantized = this.quantizer.CreateFrameQuantizer <TPixel>(image.GetConfiguration()) .QuantizeFrame(image.Frames.RootFrame); byte quantizedBits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); bits = Math.Max(bits, quantizedBits); // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk // We check again for the bit depth as the bit depth of the color palette from a given quantizer might not // be within the acceptable range. if (bits == 3) { bits = 4; } else if (bits >= 5 && bits <= 7) { bits = 8; } this.bitDepth = bits; } else { this.bitDepth = (byte)this.pngBitDepth; if (!ColorTypes[this.pngColorType.Value].Contains(this.bitDepth)) { throw new NotSupportedException("Bit depth is not supported or not valid."); } } this.bytesPerPixel = this.CalculateBytesPerPixel(); var header = new PngHeader( width: image.Width, height: image.Height, bitDepth: this.bitDepth, colorType: this.pngColorType.Value, compressionMethod: 0, // None filterMethod: 0, interlaceMethod: 0); // TODO: Can't write interlaced yet. this.WriteHeaderChunk(stream, header); // Collect the indexed pixel data if (quantized != null) { this.WritePaletteChunk(stream, quantized); } if (pngMetadata.HasTrans) { this.WriteTransparencyChunk(stream, pngMetadata); } this.WritePhysicalChunk(stream, metadata); this.WriteGammaChunk(stream); this.WriteExifChunk(stream, metadata); this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); this.WriteEndChunk(stream); stream.Flush(); quantized?.Dispose(); }