diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs
index 39fcef9c40..7692238be1 100644
--- a/src/ImageSharp/Configuration.cs
+++ b/src/ImageSharp/Configuration.cs
@@ -43,7 +43,7 @@ public sealed class Configuration
/// Initializes a new instance of the class.
///
/// A collection of configuration modules to register.
- public Configuration(params IImageFormatConfigurationModule[] configurationModules)
+ public Configuration(params IImageFormatConfigurationModule[]? configurationModules)
{
if (configurationModules != null)
{
diff --git a/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs
index e239bd8e2e..ac9d1e5602 100644
--- a/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs
+++ b/src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs
@@ -115,6 +115,26 @@ internal readonly struct APngFrameControl
}
}
+ ///
+ /// Parses the APngFrameControl from the given metadata.
+ ///
+ /// The metadata to parse.
+ /// Sequence number.
+ public static APngFrameControl FromMetadata(APngFrameMetadata frameMetadata, int sequenceNumber)
+ {
+ APngFrameControl fcTL = new(
+ sequenceNumber,
+ frameMetadata.Width,
+ frameMetadata.Height,
+ frameMetadata.XOffset,
+ frameMetadata.YOffset,
+ frameMetadata.DelayNumber,
+ frameMetadata.DelayDenominator,
+ frameMetadata.DisposeOperation,
+ frameMetadata.BlendOperation);
+ return fcTL;
+ }
+
///
/// Writes the fcTL to the given buffer.
///
@@ -126,8 +146,8 @@ internal readonly struct APngFrameControl
BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height);
BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset);
BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset);
- BinaryPrimitives.WriteInt32BigEndian(buffer[20..22], this.DelayNumber);
- BinaryPrimitives.WriteInt32BigEndian(buffer[12..24], this.DelayDenominator);
+ BinaryPrimitives.WriteInt16BigEndian(buffer[20..22], this.DelayNumber);
+ BinaryPrimitives.WriteInt16BigEndian(buffer[22..24], this.DelayDenominator);
buffer[24] = (byte)this.DisposeOperation;
buffer[25] = (byte)this.BlendOperation;
diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs
index 2c835bf8ca..866bf28af1 100644
--- a/src/ImageSharp/Formats/Png/PngChunkType.cs
+++ b/src/ImageSharp/Formats/Png/PngChunkType.cs
@@ -10,31 +10,31 @@ internal enum PngChunkType : uint
{
///
///
- /// acTL
+ /// acTL (Single)
AnimationControl = 0x6163544cU,
///
///
- /// fcTL
+ /// fcTL (Multiple)
FrameControl = 0x6663544cU,
///
///
- /// fdAT
+ /// fdAT (Multiple)
FrameData = 0x66644154U,
///
/// 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.
///
- /// IDAT
+ /// IDAT (Multiple)
Data = 0x49444154U,
///
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
///
- /// IEND
+ /// IEND (Single)
End = 0x49454E44U,
///
@@ -42,40 +42,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or
/// the used compression method.
///
- /// IHDR
+ /// IHDR (Single)
Header = 0x49484452U,
///
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
///
- /// PLTE
+ /// PLTE (Single)
Palette = 0x504C5445U,
///
/// The eXIf data chunk which contains the Exif profile.
///
- /// eXIF
+ /// eXIF (Single)
Exif = 0x65584966U,
///
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
///
- /// gAMA
+ /// gAMA (Single)
Gamma = 0x67414D41U,
///
/// This chunk specifies the intended pixel size or aspect ratio for display of the image.
///
- /// pHYs
+ /// pHYs (Single)
Physical = 0x70485973U,
///
/// 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
+ /// tEXT (Multiple)
Text = 0x74455874U,
///
@@ -83,14 +83,14 @@ internal enum PngChunkType : uint
/// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and
/// a compressed text string.
///
- /// zTXt
+ /// zTXt (Multiple)
CompressedText = 0x7A545874U,
///
/// This chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword
/// and the actual text string, which can be compressed or uncompressed.
///
- /// iTXt
+ /// iTXt (Multiple)
InternationalText = 0x69545874U,
///
@@ -98,13 +98,13 @@ internal enum PngChunkType : uint
/// either alpha values associated with palette entries (for indexed-color images)
/// or a single transparent color (for grayscale and true color images).
///
- /// tRNS
+ /// tRNS (Single)
Transparency = 0x74524E53U,
///
/// This chunk gives the time of the last image modification (not the time of initial image creation).
///
- /// tIME
+ /// tIME (Single)
Time = 0x74494d45,
///
@@ -112,47 +112,47 @@ internal enum PngChunkType : uint
/// If there is any other preferred background, either user-specified or part of a larger page (as in a browser),
/// the bKGD chunk should be ignored.
///
- /// bKGD
+ /// bKGD (Single)
Background = 0x624b4744,
///
/// This chunk contains a embedded color profile. If the iCCP chunk is present,
/// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium.
///
- /// iCCP
+ /// iCCP (Single)
EmbeddedColorProfile = 0x69434350,
///
/// This chunk defines the original number of significant bits (which can be less than or equal to the sample depth).
/// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG.
///
- /// sBIT
+ /// sBIT (Single)
SignificantBits = 0x73424954,
///
/// If the this chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed
/// using the specified rendering intent defined by the International Color Consortium.
///
- /// sRGB
+ /// sRGB (Single)
StandardRgbColourSpace = 0x73524742,
///
/// This chunk gives the approximate usage frequency of each colour in the palette.
///
- /// hIST
+ /// hIST (Single)
Histogram = 0x68495354,
///
/// This chunk contains the suggested palette.
///
- /// sPLT
+ /// sPLT (Single)
SuggestedPalette = 0x73504c54,
///
/// This chunk may be used to specify the 1931 CIE x,y chromaticities of the red,
/// green, and blue display primaries used in the image, and the referenced white point.
///
- /// cHRM
+ /// cHRM (Single)
Chroma = 0x6348524d,
///
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index 1d068303bc..bf8b23b8f0 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoder.cs
@@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-#nullable disable
using SixLabors.ImageSharp.Advanced;
@@ -18,7 +17,12 @@ public class PngEncoder : QuantizingImageEncoder
// We set the quantizer to null here to allow the underlying encoder to create a
// quantizer with options appropriate to the encoding bit depth.
- this.Quantizer = null;
+ this.Quantizer = null!;
+
+ ///
+ /// Gets whether the file is a simple PNG.
+ ///
+ public bool? IsSimplePng { get; init; }
///
/// Gets the number of bits per sample or per palette index (not per pixel).
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 8fcd1721d3..2cbc86f420 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-#nullable disable
using System.Buffers;
using System.Buffers.Binary;
@@ -9,7 +8,6 @@ using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
-using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Memory;
@@ -27,7 +25,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
///
/// The maximum block size, defaults at 64k for uncompressed blocks.
///
- private const int MaxBlockSize = 65535;
+ private const int MaxBlockSize = (1 << 16) - 1;
///
/// Used the manage memory allocations.
@@ -102,12 +100,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
///
/// The raw data of previous scanline.
///
- private IMemoryOwner previousScanline;
+ private IMemoryOwner previousScanline = null!;
///
/// The raw data of current scanline.
///
- private IMemoryOwner currentScanline;
+ private IMemoryOwner currentScanline = null!;
///
/// The color profile name.
@@ -147,34 +145,59 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
- Image clonedImage = null;
- bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear;
+ Image? clonedImage = null;
+ Image targetImage = image;
+ bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
if (clearTransparency)
{
- clonedImage = image.Clone();
- ClearTransparentPixels(clonedImage);
+ targetImage = clonedImage = image.Clone();
+ ClearTransparentPixels(targetImage);
}
- IndexedImageFrame quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage);
+ IndexedImageFrame? rootQuantized = this.CreateQuantizedImageAndUpdateBitDepth(targetImage.Frames.RootFrame);
stream.Write(PngConstants.HeaderBytes);
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
this.WriteColorProfileChunk(stream, metadata);
- this.WritePaletteChunk(stream, quantized);
+ this.WritePaletteChunk(stream, rootQuantized);
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
- this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
+
+ if (this.encoder.IsSimplePng is not true && targetImage.Frames.Count > 1)
+ {
+ this.WriteAnimationControlChunk(stream, targetImage.Frames.Count, pngMetadata.NumberPlays);
+
+ this.WriteFrameControlChunk(stream, targetImage.Frames.RootFrame.Metadata.GetAPngFrameMetadata(), 0);
+ _ = this.WriteDataChunks(targetImage.Frames.RootFrame, rootQuantized, stream, false);
+
+ int index = 1;
+
+ foreach (ImageFrame imageFrame in ((IEnumerable>)targetImage.Frames).Skip(1))
+ {
+ this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetAPngFrameMetadata(), index);
+ ++index;
+ IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(imageFrame);
+ index += this.WriteDataChunks(imageFrame, quantized, stream, true, index);
+ quantized?.Dispose();
+ }
+ }
+ else
+ {
+ _ = this.WriteDataChunks(targetImage.Frames.RootFrame, rootQuantized, stream, false);
+ rootQuantized?.Dispose();
+ }
+
this.WriteEndChunk(stream);
stream.Flush();
- quantized?.Dispose();
clonedImage?.Dispose();
+ rootQuantized?.Dispose();
}
///
@@ -182,8 +205,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
this.previousScanline?.Dispose();
this.currentScanline?.Dispose();
- this.previousScanline = null;
- this.currentScanline = null;
+ this.previousScanline = null!;
+ this.currentScanline = null!;
}
///
@@ -192,48 +215,44 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The type of the pixel.
/// The cloned image where the transparent pixels will be changed.
private static void ClearTransparentPixels(Image image)
- where TPixel : unmanaged, IPixel =>
- image.ProcessPixelRows(accessor =>
+ where TPixel : unmanaged, IPixel
+ {
+ foreach (ImageFrame imageFrame in image.Frames)
{
- // TODO: We should be able to speed this up with SIMD and masking.
- Rgba32 rgba32 = default;
- Rgba32 transparent = Color.Transparent;
- for (int y = 0; y < accessor.Height; y++)
+ imageFrame.ProcessPixelRows(accessor =>
{
- Span span = accessor.GetRowSpan(y);
- for (int x = 0; x < accessor.Width; x++)
+ // TODO: We should be able to speed this up with SIMD and masking.
+ Rgba32 rgba32 = default;
+ Rgba32 transparent = Color.Transparent;
+ for (int y = 0; y < accessor.Height; ++y)
{
- span[x].ToRgba32(ref rgba32);
-
- if (rgba32.A == 0)
+ Span span = accessor.GetRowSpan(y);
+ for (int x = 0; x < accessor.Width; ++x)
{
- span[x].FromRgba32(transparent);
+ span[x].ToRgba32(ref rgba32);
+
+ if (rgba32.A is 0)
+ {
+ span[x].FromRgba32(transparent);
+ }
}
}
- }
- });
+ });
+ }
+
+ }
///
/// Creates the quantized image and calculates and sets the bit depth.
///
/// The type of the pixel.
- /// The image to quantize.
- /// Cloned image with transparent pixels are changed to black.
+ /// The frame to quantize.
/// The quantized image.
- private IndexedImageFrame CreateQuantizedImageAndUpdateBitDepth(
- Image image,
- Image clonedImage)
+ private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth(
+ ImageFrame frame)
where TPixel : unmanaged, IPixel
{
- IndexedImageFrame quantized;
- if (this.encoder.TransparentColorMode == PngTransparentColorMode.Clear)
- {
- quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, clonedImage);
- }
- else
- {
- quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, image);
- }
+ IndexedImageFrame? quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, frame);
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized;
@@ -245,9 +264,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private void CollectGrayscaleBytes(ReadOnlySpan rowSpan)
where TPixel : unmanaged, IPixel
{
- ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span rawScanlineSpan = this.currentScanline.GetSpan();
- ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
if (this.colorType == PngColorType.Grayscale)
{
@@ -260,7 +277,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
PixelOperations.Instance.ToL16(this.configuration, rowSpan, luminanceSpan);
// Can't map directly to byte array as it's big-endian.
- for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2)
+ for (int x = 0, o = 0; x < luminanceSpan.Length; ++x, o += 2)
{
L16 luminance = Unsafe.Add(ref luminanceRef, (uint)x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue);
@@ -300,7 +317,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
PixelOperations.Instance.ToLa32(this.configuration, rowSpan, laSpan);
// Can't map directly to byte array as it's big endian.
- for (int x = 0, o = 0; x < laSpan.Length; x++, o += 4)
+ for (int x = 0, o = 0; x < laSpan.Length; ++x, o += 4)
{
La32 la = Unsafe.Add(ref laRef, (uint)x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), la.L);
@@ -403,20 +420,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The row span.
/// The quantized pixels. Can be null.
/// The row.
- private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row)
+ private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame? quantized, int row)
where TPixel : unmanaged, IPixel
{
switch (this.colorType)
{
case PngColorType.Palette:
-
if (this.bitDepth < 8)
{
- PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
+ PngEncoderHelpers.ScaleDownFrom8BitArray(quantized!.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
- quantized.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan());
+ quantized?.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan());
}
break;
@@ -477,7 +493,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ReadOnlySpan rowSpan,
ref Span filter,
ref Span attempt,
- IndexedImageFrame quantized,
+ IndexedImageFrame? quantized,
int row)
where TPixel : unmanaged, IPixel
{
@@ -577,6 +593,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size);
}
+ ///
+ /// Writes the animation control chunk to the stream.
+ ///
+ /// The containing image data.
+ /// The number of frames.
+ /// The number of times to loop this APNG.
+ private void WriteAnimationControlChunk(Stream stream, int framesCount, int playsCount)
+ {
+ APngAnimationControl acTL = new(framesCount, playsCount);
+
+ acTL.WriteTo(this.chunkDataBuffer.Span);
+
+ this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, APngAnimationControl.Size);
+ }
+
///
/// Writes the palette chunk to the stream.
/// Should be written before the first IDAT chunk.
@@ -584,7 +615,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The pixel format.
/// The containing image data.
/// The quantized frame.
- private void WritePaletteChunk(Stream stream, IndexedImageFrame quantized)
+ private void WritePaletteChunk(Stream stream, IndexedImageFrame? quantized)
where TPixel : unmanaged, IPixel
{
if (quantized is null)
@@ -692,9 +723,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return;
}
- byte[] xmpData = meta.XmpProfile.Data;
+ byte[]? xmpData = meta.XmpProfile.Data;
- if (xmpData.Length == 0)
+ if (xmpData?.Length is 0 or null)
{
return;
}
@@ -761,18 +792,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
}
const int maxLatinCode = 255;
- for (int i = 0; i < meta.TextData.Count; i++)
+ foreach (PngTextData textData in meta.TextData)
{
- PngTextData textData = meta.TextData[i];
- bool hasUnicodeCharacters = false;
- foreach (char c in textData.Value)
- {
- if (c > maxLatinCode)
- {
- hasUnicodeCharacters = true;
- break;
- }
- }
+ bool hasUnicodeCharacters = textData.Value.Any(c => c > maxLatinCode);
if (hasUnicodeCharacters || !string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))
{
@@ -876,7 +898,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
// 4-byte unsigned integer of gamma * 100,000.
uint gammaValue = (uint)(this.gamma * 100_000F);
- BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span.Slice(0, 4), gammaValue);
+ BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue);
this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4);
}
@@ -896,51 +918,69 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
}
Span alpha = this.chunkDataBuffer.Span;
- if (pngMetadata.ColorType == PngColorType.Rgb)
+ switch (pngMetadata.ColorType)
{
- if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit)
- {
- Rgb48 rgb = pngMetadata.TransparentRgb48.Value;
- BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R);
- BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
- BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
+ case PngColorType.Rgb when pngMetadata.TransparentRgb48.HasValue && this.use16Bit:
+ Rgb48 rgb48 = pngMetadata.TransparentRgb48.Value;
+ BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb48.R);
+ BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb48.G);
+ BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb48.B);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
- }
- else if (pngMetadata.TransparentRgb24.HasValue)
- {
- alpha.Clear();
- Rgb24 rgb = pngMetadata.TransparentRgb24.Value;
- alpha[1] = rgb.R;
- alpha[3] = rgb.G;
- alpha[5] = rgb.B;
- this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
- }
- }
- else if (pngMetadata.ColorType == PngColorType.Grayscale)
- {
- if (pngMetadata.TransparentL16.HasValue && this.use16Bit)
- {
+ break;
+ case PngColorType.Rgb:
+ if (pngMetadata.TransparentRgb24.HasValue)
+ {
+ alpha.Clear();
+ Rgb24 rgb24 = pngMetadata.TransparentRgb24.Value;
+ alpha[1] = rgb24.R;
+ alpha[3] = rgb24.G;
+ alpha[5] = rgb24.B;
+ this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
+ }
+
+ break;
+ case PngColorType.Grayscale when pngMetadata.TransparentL16.HasValue && this.use16Bit:
BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
- }
- else if (pngMetadata.TransparentL8.HasValue)
- {
- alpha.Clear();
- alpha[1] = pngMetadata.TransparentL8.Value.PackedValue;
- this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
- }
+ break;
+ case PngColorType.Grayscale:
+ if (pngMetadata.TransparentL8.HasValue)
+ {
+ alpha.Clear();
+ alpha[1] = pngMetadata.TransparentL8.Value.PackedValue;
+ this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
+ }
+
+ break;
}
}
+ ///
+ /// Writes the animation control chunk to the stream.
+ ///
+ /// The containing image data.
+ /// Provides APng specific metadata information for the image frame.
+ /// Sequence number.
+ private void WriteFrameControlChunk(Stream stream, APngFrameMetadata frameMetadata, int sequenceNumber)
+ {
+ APngFrameControl fcTL = APngFrameControl.FromMetadata(frameMetadata, sequenceNumber);
+
+ fcTL.WriteTo(this.chunkDataBuffer.Span);
+
+ this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, APngFrameControl.Size);
+ }
+
///
/// Writes the pixel information to the stream.
///
/// The pixel format.
- /// The image.
+ /// The frame.
/// The quantized pixel data. Can be null.
/// The stream.
- private void WriteDataChunks(Image pixels, IndexedImageFrame quantized, Stream stream)
+ /// Is writing fdAT or IDAT.
+ /// Start sequence number.
+ private int WriteDataChunks(ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame, int startSequenceNumber = 0)
where TPixel : unmanaged, IPixel
{
byte[] buffer;
@@ -950,9 +990,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel))
{
- if (this.interlaceMode == PngInterlaceMode.Adam7)
+ if (this.interlaceMode is PngInterlaceMode.Adam7)
{
- if (quantized != null)
+ if (quantized is not null)
{
this.EncodeAdam7IndexedPixels(quantized, deflateStream);
}
@@ -973,24 +1013,43 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
// Store the chunks in repeated 64k blocks.
// This reduces the memory load for decoding the image for many decoders.
- int numChunks = bufferLength / MaxBlockSize;
+ int maxBlockSize = MaxBlockSize;
+ if (isFrame)
+ {
+ maxBlockSize -= 4;
+ }
- if (bufferLength % MaxBlockSize != 0)
+ int numChunks = bufferLength / maxBlockSize;
+
+ if (bufferLength % maxBlockSize != 0)
{
- numChunks++;
+ ++numChunks;
}
- for (int i = 0; i < numChunks; i++)
+ for (int i = 0; i < numChunks; ++i)
{
- int length = bufferLength - (i * MaxBlockSize);
+ int length = bufferLength - (i * maxBlockSize);
- if (length > MaxBlockSize)
+ if (length > maxBlockSize)
{
- length = MaxBlockSize;
+ length = maxBlockSize;
}
- this.WriteChunk(stream, PngChunkType.Data, buffer, i * MaxBlockSize, length);
+ if (isFrame)
+ {
+ byte[] chunkBuffer = new byte[MaxBlockSize];
+ BinaryPrimitives.WriteInt32BigEndian(chunkBuffer, startSequenceNumber + i);
+ buffer.AsSpan().Slice(i * maxBlockSize, length).CopyTo(chunkBuffer.AsSpan(4, length));
+
+ this.WriteChunk(stream, PngChunkType.FrameData, chunkBuffer, 0, length + 4);
+ }
+ else
+ {
+ this.WriteChunk(stream, PngChunkType.Data, buffer, i * maxBlockSize, length);
+ }
}
+
+ return numChunks;
}
///
@@ -1013,10 +1072,18 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The pixels.
/// The quantized pixels span.
/// The deflate stream.
- private void EncodePixels(Image pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
+ private void EncodePixels(ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int bytesPerScanline = this.CalculateScanlineLength(this.width);
+ int width = this.width;
+ int height = this.height;
+ if (pixels.Metadata.TryGetAPngFrameMetadata(out APngFrameMetadata? pngMetadata))
+ {
+ width = pngMetadata.Width;
+ height = pngMetadata.Height;
+ }
+
+ int bytesPerScanline = this.CalculateScanlineLength(width);
int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline);
@@ -1027,7 +1094,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
Span filter = filterBuffer.GetSpan();
Span attempt = attemptBuffer.GetSpan();
- for (int y = 0; y < this.height; y++)
+ for (int y = 0; y < height; ++y)
{
this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y);
deflateStream.Write(filter);
@@ -1040,14 +1107,14 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Interlaced encoding the pixels.
///
/// The type of the pixel.
- /// The image.
+ /// The image frame.
/// The deflate stream.
- private void EncodeAdam7Pixels(Image image, ZlibDeflateStream deflateStream)
+ private void EncodeAdam7Pixels(ImageFrame frame, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
- Buffer2D pixelBuffer = image.Frames.RootFrame.PixelBuffer;
+ int width = frame.Width;
+ int height = frame.Height;
+ Buffer2D pixelBuffer = frame.PixelBuffer;
for (int pass = 0; pass < 7; pass++)
{
int startRow = Adam7.FirstRow[pass];
@@ -1132,7 +1199,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
col < width;
col += Adam7.ColumnIncrement[pass])
{
- block[i++] = srcRow[col];
+ block[i] = srcRow[col];
+ ++i;
}
// Encode data
@@ -1176,7 +1244,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
stream.Write(buffer);
- uint crc = Crc32.Calculate(buffer.Slice(4)); // Write the type buffer
+ uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer
if (data.Length > 0 && length > 0)
{
@@ -1199,7 +1267,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
///
private int CalculateScanlineLength(int width)
{
- int mod = this.bitDepth == 16 ? 16 : 8;
+ int mod = this.bitDepth is 16 ? 16 : 8;
int scanlineLength = width * this.bitDepth * this.bytesPerPixel;
int amount = scanlineLength % mod;
@@ -1243,14 +1311,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (!encoder.FilterMethod.HasValue)
{
// Specification recommends default filter method None for paletted images and Paeth for others.
- if (this.colorType == PngColorType.Palette)
- {
- this.filterMethod = PngFilterMethod.None;
- }
- else
- {
- this.filterMethod = PngFilterMethod.Paeth;
- }
+ this.filterMethod = this.colorType is PngColorType.Palette ? PngFilterMethod.None : PngFilterMethod.Paeth;
}
// Ensure bit depth and color type are a supported combination.
@@ -1266,7 +1327,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
use16Bit = bits == (byte)PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit);
- this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod).Value;
+ this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod)!.Value;
this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None;
}
@@ -1277,28 +1338,29 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The png encoder.
/// The color type.
/// The bits per component.
- /// The image.
- private static IndexedImageFrame CreateQuantizedFrame(
+ /// The frame.
+ private static IndexedImageFrame? CreateQuantizedFrame(
QuantizingImageEncoder encoder,
PngColorType colorType,
byte bitDepth,
- Image image)
+ ImageFrame frame)
where TPixel : unmanaged, IPixel
{
- if (colorType != PngColorType.Palette)
+ if (colorType is not PngColorType.Palette)
{
return null;
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
+ // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
IQuantizer quantizer = encoder.Quantizer
- ?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
+ ?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
// Create quantized frame returning the palette and set the bit depth.
- using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(image.GetConfiguration());
+ using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(frame.GetConfiguration());
- frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image);
- return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
+ return frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
///
@@ -1312,25 +1374,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private static byte CalculateBitDepth(
PngColorType colorType,
byte bitDepth,
- IndexedImageFrame quantizedFrame)
+ IndexedImageFrame? quantizedFrame)
where TPixel : unmanaged, IPixel
{
- if (colorType == PngColorType.Palette)
+ if (colorType is PngColorType.Palette)
{
- byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame.Palette.Length), 1, 8);
+ byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame!.Palette.Length), 1, 8);
byte bits = Math.Max(bitDepth, 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 = bits switch
{
- bits = 4;
- }
- else if (bits is >= 5 and <= 7)
- {
- bits = 8;
- }
+ 3 => 4,
+ >= 5 and <= 7 => 8,
+ _ => bits
+ };
bitDepth = bits;
}
@@ -1368,21 +1428,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The type of pixel format.
private static PngColorType SuggestColorType()
where TPixel : unmanaged, IPixel
- => typeof(TPixel) switch
+ => default(TPixel) switch
{
- Type t when t == typeof(A8) => PngColorType.GrayscaleWithAlpha,
- Type t when t == typeof(Argb32) => PngColorType.RgbWithAlpha,
- Type t when t == typeof(Bgr24) => PngColorType.Rgb,
- Type t when t == typeof(Bgra32) => PngColorType.RgbWithAlpha,
- Type t when t == typeof(L8) => PngColorType.Grayscale,
- Type t when t == typeof(L16) => PngColorType.Grayscale,
- Type t when t == typeof(La16) => PngColorType.GrayscaleWithAlpha,
- Type t when t == typeof(La32) => PngColorType.GrayscaleWithAlpha,
- Type t when t == typeof(Rgb24) => PngColorType.Rgb,
- Type t when t == typeof(Rgba32) => PngColorType.RgbWithAlpha,
- Type t when t == typeof(Rgb48) => PngColorType.Rgb,
- Type t when t == typeof(Rgba64) => PngColorType.RgbWithAlpha,
- Type t when t == typeof(RgbaVector) => PngColorType.RgbWithAlpha,
+ A8 => PngColorType.GrayscaleWithAlpha,
+ Argb32 => PngColorType.RgbWithAlpha,
+ Bgr24 => PngColorType.Rgb,
+ Bgra32 => PngColorType.RgbWithAlpha,
+ L8 => PngColorType.Grayscale,
+ L16 => PngColorType.Grayscale,
+ La16 => PngColorType.GrayscaleWithAlpha,
+ La32 => PngColorType.GrayscaleWithAlpha,
+ Rgb24 => PngColorType.Rgb,
+ Rgba32 => PngColorType.RgbWithAlpha,
+ Rgb48 => PngColorType.Rgb,
+ Rgba64 => PngColorType.RgbWithAlpha,
+ RgbaVector => PngColorType.RgbWithAlpha,
_ => PngColorType.RgbWithAlpha
};
@@ -1393,27 +1453,27 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The type of pixel format.
private static PngBitDepth SuggestBitDepth()
where TPixel : unmanaged, IPixel
- => typeof(TPixel) switch
+ => default(TPixel) switch
{
- Type t when t == typeof(A8) => PngBitDepth.Bit8,
- Type t when t == typeof(Argb32) => PngBitDepth.Bit8,
- Type t when t == typeof(Bgr24) => PngBitDepth.Bit8,
- Type t when t == typeof(Bgra32) => PngBitDepth.Bit8,
- Type t when t == typeof(L8) => PngBitDepth.Bit8,
- Type t when t == typeof(L16) => PngBitDepth.Bit16,
- Type t when t == typeof(La16) => PngBitDepth.Bit8,
- Type t when t == typeof(La32) => PngBitDepth.Bit16,
- Type t when t == typeof(Rgb24) => PngBitDepth.Bit8,
- Type t when t == typeof(Rgba32) => PngBitDepth.Bit8,
- Type t when t == typeof(Rgb48) => PngBitDepth.Bit16,
- Type t when t == typeof(Rgba64) => PngBitDepth.Bit16,
- Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16,
+ A8 => PngBitDepth.Bit8,
+ Argb32 => PngBitDepth.Bit8,
+ Bgr24 => PngBitDepth.Bit8,
+ Bgra32 => PngBitDepth.Bit8,
+ L8 => PngBitDepth.Bit8,
+ L16 => PngBitDepth.Bit16,
+ La16 => PngBitDepth.Bit8,
+ La32 => PngBitDepth.Bit16,
+ Rgb24 => PngBitDepth.Bit8,
+ Rgba32 => PngBitDepth.Bit8,
+ Rgb48 => PngBitDepth.Bit16,
+ Rgba64 => PngBitDepth.Bit16,
+ RgbaVector => PngBitDepth.Bit16,
_ => PngBitDepth.Bit8
};
private unsafe struct ScratchBuffer
{
- private const int Size = 16;
+ private const int Size = 26;
private fixed byte scratch[Size];
public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index 9f874d5c9a..c4d136505f 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index 2dfd99439a..2fc0dc0c06 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -7,7 +7,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
@@ -107,6 +106,27 @@ public partial class PngDecoderTests
image.CompareToOriginal(provider, ImageComparer.Exact);
}
+ [Theory]
+ [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)]
+ public void Decode_APng(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder.Instance);
+ image.SaveAsPng("C:\\WorkSpace\\1.png");
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact);
+ // TODO test
+ }
+
+ [Theory]
+ [WithFile("C:\\WorkSpace\\Fuck.png", PixelTypes.Rgba32)]
+ public void Decode_APng2(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder.Instance);
+ image.SaveAsPng("C:\\WorkSpace\\1.png");
+ }
+
[Theory]
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
public void PngDecoder_Decode_Resize(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
index ff81401f56..4492934f1a 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
@@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
-using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Metadata;
@@ -134,34 +133,6 @@ public class PngMetadataTests
VerifyExifDataIsPresent(exif);
}
- [Theory]
- [WithFile(@"C:\WorkSpace\App1\App1\Assets\7.png", PixelTypes.Rgba32)]
- public void Decode_ReadsExifData2(TestImageProvider provider)
- where TPixel : unmanaged, IPixel
- {
- DecoderOptions options = new()
- {
- SkipMetadata = false
- };
-
- using Image image = provider.GetImage(PngDecoder.Instance, options);
- TPixel pixel = image.Frames.RootFrame[5, 5];
- TPixel pixel2 = image.Frames[1][5, 5];
- }
-
- [Theory]
- [WithFile(@"Png\pl.png", PixelTypes.Rgba32)]
- public void Decode_ReadsExifData3(TestImageProvider provider)
- where TPixel : unmanaged, IPixel
- {
- DecoderOptions options = new()
- {
- SkipMetadata = false
- };
-
- using Image image = provider.GetImage(PngDecoder.Instance, options);
- }
-
[Theory]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
index 96b5b620b8..878f3fb8d4 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
namespace SixLabors.ImageSharp.Tests.Formats.Png;
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index a25424b6d9..2ddcc559b8 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -61,6 +61,7 @@ public static class TestImages
public const string TestPattern31x31 = "Png/testpattern31x31.png";
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";
public const string XmpColorPalette = "Png/xmp-colorpalette.png";
+ public const string APng = "Png/apng.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";
diff --git a/tests/Images/Input/Png/apng.png b/tests/Images/Input/Png/apng.png
new file mode 100644
index 0000000000..c5b2adf8e9
--- /dev/null
+++ b/tests/Images/Input/Png/apng.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f6b0e5a904e269a9108b32c0f5cc98cda4240a60db421f560f45d2e36ead32a9
+size 212