Browse Source

implement APNG encoder

pull/2511/head
Poker 3 years ago
parent
commit
7b6c32d54b
No known key found for this signature in database GPG Key ID: 720AFAD63099D9CB
  1. 2
      src/ImageSharp/Configuration.cs
  2. 24
      src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs
  3. 44
      src/ImageSharp/Formats/Png/PngChunkType.cs
  4. 8
      src/ImageSharp/Formats/Png/PngEncoder.cs
  5. 410
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  6. 1
      src/ImageSharp/Formats/Png/PngMetadata.cs
  7. 22
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  8. 29
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  9. 1
      tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
  10. 1
      tests/ImageSharp.Tests/TestImages.cs
  11. 3
      tests/Images/Input/Png/apng.png

2
src/ImageSharp/Configuration.cs

@ -43,7 +43,7 @@ public sealed class Configuration
/// Initializes a new instance of the <see cref="Configuration" /> class. /// Initializes a new instance of the <see cref="Configuration" /> class.
/// </summary> /// </summary>
/// <param name="configurationModules">A collection of configuration modules to register.</param> /// <param name="configurationModules">A collection of configuration modules to register.</param>
public Configuration(params IImageFormatConfigurationModule[] configurationModules) public Configuration(params IImageFormatConfigurationModule[]? configurationModules)
{ {
if (configurationModules != null) if (configurationModules != null)
{ {

24
src/ImageSharp/Formats/Png/Chunks/APngFrameControl.cs

@ -115,6 +115,26 @@ internal readonly struct APngFrameControl
} }
} }
/// <summary>
/// Parses the APngFrameControl from the given metadata.
/// </summary>
/// <param name="frameMetadata">The metadata to parse.</param>
/// <param name="sequenceNumber">Sequence number.</param>
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;
}
/// <summary> /// <summary>
/// Writes the fcTL to the given buffer. /// Writes the fcTL to the given buffer.
/// </summary> /// </summary>
@ -126,8 +146,8 @@ internal readonly struct APngFrameControl
BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height); BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height);
BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset); BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset);
BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset); BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset);
BinaryPrimitives.WriteInt32BigEndian(buffer[20..22], this.DelayNumber); BinaryPrimitives.WriteInt16BigEndian(buffer[20..22], this.DelayNumber);
BinaryPrimitives.WriteInt32BigEndian(buffer[12..24], this.DelayDenominator); BinaryPrimitives.WriteInt16BigEndian(buffer[22..24], this.DelayDenominator);
buffer[24] = (byte)this.DisposeOperation; buffer[24] = (byte)this.DisposeOperation;
buffer[25] = (byte)this.BlendOperation; buffer[25] = (byte)this.BlendOperation;

44
src/ImageSharp/Formats/Png/PngChunkType.cs

@ -10,31 +10,31 @@ internal enum PngChunkType : uint
{ {
/// <summary> /// <summary>
/// </summary> /// </summary>
/// <remarks>acTL</remarks> /// <remarks>acTL (Single)</remarks>
AnimationControl = 0x6163544cU, AnimationControl = 0x6163544cU,
/// <summary> /// <summary>
/// </summary> /// </summary>
/// <remarks>fcTL</remarks> /// <remarks>fcTL (Multiple)</remarks>
FrameControl = 0x6663544cU, FrameControl = 0x6663544cU,
/// <summary> /// <summary>
/// </summary> /// </summary>
/// <remarks>fdAT</remarks> /// <remarks>fdAT (Multiple)</remarks>
FrameData = 0x66644154U, FrameData = 0x66644154U,
/// <summary> /// <summary>
/// The IDAT chunk contains the actual image data. The image can contains more /// 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. /// than one chunk of this type. All chunks together are the whole image.
/// </summary> /// </summary>
/// <remarks>IDAT</remarks> /// <remarks>IDAT (Multiple)</remarks>
Data = 0x49444154U, Data = 0x49444154U,
/// <summary> /// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream. /// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty. /// The chunk's data field is empty.
/// </summary> /// </summary>
/// <remarks>IEND</remarks> /// <remarks>IEND (Single)</remarks>
End = 0x49454E44U, End = 0x49454E44U,
/// <summary> /// <summary>
@ -42,40 +42,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or /// common information like the width and the height of the image or
/// the used compression method. /// the used compression method.
/// </summary> /// </summary>
/// <remarks>IHDR</remarks> /// <remarks>IHDR (Single)</remarks>
Header = 0x49484452U, Header = 0x49484452U,
/// <summary> /// <summary>
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format. /// series in the RGB format.
/// </summary> /// </summary>
/// <remarks>PLTE</remarks> /// <remarks>PLTE (Single)</remarks>
Palette = 0x504C5445U, Palette = 0x504C5445U,
/// <summary> /// <summary>
/// The eXIf data chunk which contains the Exif profile. /// The eXIf data chunk which contains the Exif profile.
/// </summary> /// </summary>
/// <remarks>eXIF</remarks> /// <remarks>eXIF (Single)</remarks>
Exif = 0x65584966U, Exif = 0x65584966U,
/// <summary> /// <summary>
/// This chunk specifies the relationship between the image samples and the desired /// This chunk specifies the relationship between the image samples and the desired
/// display output intensity. /// display output intensity.
/// </summary> /// </summary>
/// <remarks>gAMA</remarks> /// <remarks>gAMA (Single)</remarks>
Gamma = 0x67414D41U, Gamma = 0x67414D41U,
/// <summary> /// <summary>
/// This chunk specifies the intended pixel size or aspect ratio for display of the image. /// This chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary> /// </summary>
/// <remarks>pHYs</remarks> /// <remarks>pHYs (Single)</remarks>
Physical = 0x70485973U, Physical = 0x70485973U,
/// <summary> /// <summary>
/// Textual information that the encoder wishes to record with the image can be stored in /// 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 chunks. Each tEXt chunk contains a keyword and a text string.
/// </summary> /// </summary>
/// <remarks>tEXT</remarks> /// <remarks>tEXT (Multiple)</remarks>
Text = 0x74455874U, Text = 0x74455874U,
/// <summary> /// <summary>
@ -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 /// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and
/// a compressed text string. /// a compressed text string.
/// </summary> /// </summary>
/// <remarks>zTXt</remarks> /// <remarks>zTXt (Multiple)</remarks>
CompressedText = 0x7A545874U, CompressedText = 0x7A545874U,
/// <summary> /// <summary>
/// This chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword /// 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. /// and the actual text string, which can be compressed or uncompressed.
/// </summary> /// </summary>
/// <remarks>iTXt</remarks> /// <remarks>iTXt (Multiple)</remarks>
InternationalText = 0x69545874U, InternationalText = 0x69545874U,
/// <summary> /// <summary>
@ -98,13 +98,13 @@ internal enum PngChunkType : uint
/// either alpha values associated with palette entries (for indexed-color images) /// either alpha values associated with palette entries (for indexed-color images)
/// or a single transparent color (for grayscale and true color images). /// or a single transparent color (for grayscale and true color images).
/// </summary> /// </summary>
/// <remarks>tRNS</remarks> /// <remarks>tRNS (Single)</remarks>
Transparency = 0x74524E53U, Transparency = 0x74524E53U,
/// <summary> /// <summary>
/// This chunk gives the time of the last image modification (not the time of initial image creation). /// This chunk gives the time of the last image modification (not the time of initial image creation).
/// </summary> /// </summary>
/// <remarks>tIME</remarks> /// <remarks>tIME (Single)</remarks>
Time = 0x74494d45, Time = 0x74494d45,
/// <summary> /// <summary>
@ -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), /// 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. /// the bKGD chunk should be ignored.
/// </summary> /// </summary>
/// <remarks>bKGD</remarks> /// <remarks>bKGD (Single)</remarks>
Background = 0x624b4744, Background = 0x624b4744,
/// <summary> /// <summary>
/// This chunk contains a embedded color profile. If the iCCP chunk is present, /// 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. /// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium.
/// </summary> /// </summary>
/// <remarks>iCCP</remarks> /// <remarks>iCCP (Single)</remarks>
EmbeddedColorProfile = 0x69434350, EmbeddedColorProfile = 0x69434350,
/// <summary> /// <summary>
/// This chunk defines the original number of significant bits (which can be less than or equal to the sample depth). /// 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. /// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG.
/// </summary> /// </summary>
/// <remarks>sBIT</remarks> /// <remarks>sBIT (Single)</remarks>
SignificantBits = 0x73424954, SignificantBits = 0x73424954,
/// <summary> /// <summary>
/// If the this chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed /// 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. /// using the specified rendering intent defined by the International Color Consortium.
/// </summary> /// </summary>
/// <remarks>sRGB</remarks> /// <remarks>sRGB (Single)</remarks>
StandardRgbColourSpace = 0x73524742, StandardRgbColourSpace = 0x73524742,
/// <summary> /// <summary>
/// This chunk gives the approximate usage frequency of each colour in the palette. /// This chunk gives the approximate usage frequency of each colour in the palette.
/// </summary> /// </summary>
/// <remarks>hIST</remarks> /// <remarks>hIST (Single)</remarks>
Histogram = 0x68495354, Histogram = 0x68495354,
/// <summary> /// <summary>
/// This chunk contains the suggested palette. /// This chunk contains the suggested palette.
/// </summary> /// </summary>
/// <remarks>sPLT</remarks> /// <remarks>sPLT (Single)</remarks>
SuggestedPalette = 0x73504c54, SuggestedPalette = 0x73504c54,
/// <summary> /// <summary>
/// This chunk may be used to specify the 1931 CIE x,y chromaticities of the red, /// 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. /// green, and blue display primaries used in the image, and the referenced white point.
/// </summary> /// </summary>
/// <remarks>cHRM</remarks> /// <remarks>cHRM (Single)</remarks>
Chroma = 0x6348524d, Chroma = 0x6348524d,
/// <summary> /// <summary>

8
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using SixLabors.ImageSharp.Advanced; 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 // We set the quantizer to null here to allow the underlying encoder to create a
// quantizer with options appropriate to the encoding bit depth. // quantizer with options appropriate to the encoding bit depth.
this.Quantizer = null; this.Quantizer = null!;
/// <summary>
/// Gets whether the file is a simple PNG.
/// </summary>
public bool? IsSimplePng { get; init; }
/// <summary> /// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel). /// Gets the number of bits per sample or per palette index (not per pixel).

410
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
@ -9,7 +8,6 @@ using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -27,7 +25,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <summary> /// <summary>
/// The maximum block size, defaults at 64k for uncompressed blocks. /// The maximum block size, defaults at 64k for uncompressed blocks.
/// </summary> /// </summary>
private const int MaxBlockSize = 65535; private const int MaxBlockSize = (1 << 16) - 1;
/// <summary> /// <summary>
/// Used the manage memory allocations. /// Used the manage memory allocations.
@ -102,12 +100,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <summary> /// <summary>
/// The raw data of previous scanline. /// The raw data of previous scanline.
/// </summary> /// </summary>
private IMemoryOwner<byte> previousScanline; private IMemoryOwner<byte> previousScanline = null!;
/// <summary> /// <summary>
/// The raw data of current scanline. /// The raw data of current scanline.
/// </summary> /// </summary>
private IMemoryOwner<byte> currentScanline; private IMemoryOwner<byte> currentScanline = null!;
/// <summary> /// <summary>
/// The color profile name. /// The color profile name.
@ -147,34 +145,59 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
Image<TPixel> clonedImage = null; Image<TPixel>? clonedImage = null;
bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear; Image<TPixel> targetImage = image;
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
if (clearTransparency) if (clearTransparency)
{ {
clonedImage = image.Clone(); targetImage = clonedImage = image.Clone();
ClearTransparentPixels(clonedImage); ClearTransparentPixels(targetImage);
} }
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage); IndexedImageFrame<TPixel>? rootQuantized = this.CreateQuantizedImageAndUpdateBitDepth(targetImage.Frames.RootFrame);
stream.Write(PngConstants.HeaderBytes); stream.Write(PngConstants.HeaderBytes);
this.WriteHeaderChunk(stream); this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream); this.WriteGammaChunk(stream);
this.WriteColorProfileChunk(stream, metadata); this.WriteColorProfileChunk(stream, metadata);
this.WritePaletteChunk(stream, quantized); this.WritePaletteChunk(stream, rootQuantized);
this.WriteTransparencyChunk(stream, pngMetadata); this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata); this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata); this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata); this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata); 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<TPixel> imageFrame in ((IEnumerable<ImageFrame<TPixel>>)targetImage.Frames).Skip(1))
{
this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetAPngFrameMetadata(), index);
++index;
IndexedImageFrame<TPixel>? 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); this.WriteEndChunk(stream);
stream.Flush(); stream.Flush();
quantized?.Dispose();
clonedImage?.Dispose(); clonedImage?.Dispose();
rootQuantized?.Dispose();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -182,8 +205,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
this.previousScanline?.Dispose(); this.previousScanline?.Dispose();
this.currentScanline?.Dispose(); this.currentScanline?.Dispose();
this.previousScanline = null; this.previousScanline = null!;
this.currentScanline = null; this.currentScanline = null!;
} }
/// <summary> /// <summary>
@ -192,48 +215,44 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The cloned image where the transparent pixels will be changed.</param> /// <param name="image">The cloned image where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(Image<TPixel> image) private static void ClearTransparentPixels<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> => where TPixel : unmanaged, IPixel<TPixel>
image.ProcessPixelRows(accessor => {
foreach (ImageFrame<TPixel> imageFrame in image.Frames)
{ {
// TODO: We should be able to speed this up with SIMD and masking. imageFrame.ProcessPixelRows(accessor =>
Rgba32 rgba32 = default;
Rgba32 transparent = Color.Transparent;
for (int y = 0; y < accessor.Height; y++)
{ {
Span<TPixel> span = accessor.GetRowSpan(y); // TODO: We should be able to speed this up with SIMD and masking.
for (int x = 0; x < accessor.Width; x++) Rgba32 rgba32 = default;
Rgba32 transparent = Color.Transparent;
for (int y = 0; y < accessor.Height; ++y)
{ {
span[x].ToRgba32(ref rgba32); Span<TPixel> span = accessor.GetRowSpan(y);
for (int x = 0; x < accessor.Width; ++x)
if (rgba32.A == 0)
{ {
span[x].FromRgba32(transparent); span[x].ToRgba32(ref rgba32);
if (rgba32.A is 0)
{
span[x].FromRgba32(transparent);
}
} }
} }
} });
}); }
}
/// <summary> /// <summary>
/// Creates the quantized image and calculates and sets the bit depth. /// Creates the quantized image and calculates and sets the bit depth.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image to quantize.</param> /// <param name="frame">The frame to quantize.</param>
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param>
/// <returns>The quantized image.</returns> /// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel> CreateQuantizedImageAndUpdateBitDepth<TPixel>( private IndexedImageFrame<TPixel>? CreateQuantizedImageAndUpdateBitDepth<TPixel>(
Image<TPixel> image, ImageFrame<TPixel> frame)
Image<TPixel> clonedImage)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
IndexedImageFrame<TPixel> quantized; IndexedImageFrame<TPixel>? quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, frame);
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);
}
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized; return quantized;
@ -245,9 +264,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private void CollectGrayscaleBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan) private void CollectGrayscaleBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span<byte> rawScanlineSpan = this.currentScanline.GetSpan(); Span<byte> rawScanlineSpan = this.currentScanline.GetSpan();
ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
if (this.colorType == PngColorType.Grayscale) if (this.colorType == PngColorType.Grayscale)
{ {
@ -260,7 +277,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
PixelOperations<TPixel>.Instance.ToL16(this.configuration, rowSpan, luminanceSpan); PixelOperations<TPixel>.Instance.ToL16(this.configuration, rowSpan, luminanceSpan);
// Can't map directly to byte array as it's big-endian. // 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); L16 luminance = Unsafe.Add(ref luminanceRef, (uint)x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue);
@ -300,7 +317,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
PixelOperations<TPixel>.Instance.ToLa32(this.configuration, rowSpan, laSpan); PixelOperations<TPixel>.Instance.ToLa32(this.configuration, rowSpan, laSpan);
// Can't map directly to byte array as it's big endian. // 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); La32 la = Unsafe.Add(ref laRef, (uint)x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), la.L); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), la.L);
@ -403,20 +420,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="rowSpan">The row span.</param> /// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param> /// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param> /// <param name="row">The row.</param>
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row) private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel>? quantized, int row)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
switch (this.colorType) switch (this.colorType)
{ {
case PngColorType.Palette: case PngColorType.Palette:
if (this.bitDepth < 8) 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 else
{ {
quantized.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan()); quantized?.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan());
} }
break; break;
@ -477,7 +493,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ReadOnlySpan<TPixel> rowSpan, ReadOnlySpan<TPixel> rowSpan,
ref Span<byte> filter, ref Span<byte> filter,
ref Span<byte> attempt, ref Span<byte> attempt,
IndexedImageFrame<TPixel> quantized, IndexedImageFrame<TPixel>? quantized,
int row) int row)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -577,6 +593,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size); this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size);
} }
/// <summary>
/// Writes the animation control chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="framesCount">The number of frames.</param>
/// <param name="playsCount">The number of times to loop this APNG.</param>
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);
}
/// <summary> /// <summary>
/// Writes the palette chunk to the stream. /// Writes the palette chunk to the stream.
/// Should be written before the first IDAT chunk. /// Should be written before the first IDAT chunk.
@ -584,7 +615,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="quantized">The quantized frame.</param> /// <param name="quantized">The quantized frame.</param>
private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel> quantized) private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel>? quantized)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (quantized is null) if (quantized is null)
@ -692,9 +723,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return; return;
} }
byte[] xmpData = meta.XmpProfile.Data; byte[]? xmpData = meta.XmpProfile.Data;
if (xmpData.Length == 0) if (xmpData?.Length is 0 or null)
{ {
return; return;
} }
@ -761,18 +792,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
} }
const int maxLatinCode = 255; 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 = textData.Value.Any(c => c > maxLatinCode);
bool hasUnicodeCharacters = false;
foreach (char c in textData.Value)
{
if (c > maxLatinCode)
{
hasUnicodeCharacters = true;
break;
}
}
if (hasUnicodeCharacters || !string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)) 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. // 4-byte unsigned integer of gamma * 100,000.
uint gammaValue = (uint)(this.gamma * 100_000F); 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); this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4);
} }
@ -896,51 +918,69 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
} }
Span<byte> alpha = this.chunkDataBuffer.Span; Span<byte> alpha = this.chunkDataBuffer.Span;
if (pngMetadata.ColorType == PngColorType.Rgb) switch (pngMetadata.ColorType)
{ {
if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit) case PngColorType.Rgb when pngMetadata.TransparentRgb48.HasValue && this.use16Bit:
{ Rgb48 rgb48 = pngMetadata.TransparentRgb48.Value;
Rgb48 rgb = pngMetadata.TransparentRgb48.Value; BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb48.R);
BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb48.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb48.B);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
} break;
else if (pngMetadata.TransparentRgb24.HasValue) case PngColorType.Rgb:
{ if (pngMetadata.TransparentRgb24.HasValue)
alpha.Clear(); {
Rgb24 rgb = pngMetadata.TransparentRgb24.Value; alpha.Clear();
alpha[1] = rgb.R; Rgb24 rgb24 = pngMetadata.TransparentRgb24.Value;
alpha[3] = rgb.G; alpha[1] = rgb24.R;
alpha[5] = rgb.B; alpha[3] = rgb24.G;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); alpha[5] = rgb24.B;
} this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
} }
else if (pngMetadata.ColorType == PngColorType.Grayscale)
{ break;
if (pngMetadata.TransparentL16.HasValue && this.use16Bit) case PngColorType.Grayscale when pngMetadata.TransparentL16.HasValue && this.use16Bit:
{
BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue); BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
} break;
else if (pngMetadata.TransparentL8.HasValue) case PngColorType.Grayscale:
{ if (pngMetadata.TransparentL8.HasValue)
alpha.Clear(); {
alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; alpha.Clear();
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); alpha[1] = pngMetadata.TransparentL8.Value.PackedValue;
} this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
}
break;
} }
} }
/// <summary>
/// Writes the animation control chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="frameMetadata">Provides APng specific metadata information for the image frame.</param>
/// <param name="sequenceNumber">Sequence number.</param>
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);
}
/// <summary> /// <summary>
/// Writes the pixel information to the stream. /// Writes the pixel information to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The image.</param> /// <param name="pixels">The frame.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param> /// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream) /// <param name="isFrame">Is writing fdAT or IDAT.</param>
/// <param name="startSequenceNumber">Start sequence number.</param>
private int WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, Stream stream, bool isFrame, int startSequenceNumber = 0)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte[] buffer; byte[] buffer;
@ -950,9 +990,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) 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); this.EncodeAdam7IndexedPixels(quantized, deflateStream);
} }
@ -973,24 +1013,43 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
// Store the chunks in repeated 64k blocks. // Store the chunks in repeated 64k blocks.
// This reduces the memory load for decoding the image for many decoders. // 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;
} }
/// <summary> /// <summary>
@ -1013,10 +1072,18 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="pixels">The pixels.</param> /// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param> /// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</param> /// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream) private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
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; int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline); this.AllocateScanlineBuffers(bytesPerScanline);
@ -1027,7 +1094,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
Span<byte> filter = filterBuffer.GetSpan(); Span<byte> filter = filterBuffer.GetSpan();
Span<byte> attempt = attemptBuffer.GetSpan(); Span<byte> 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); this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y);
deflateStream.Write(filter); deflateStream.Write(filter);
@ -1040,14 +1107,14 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Interlaced encoding the pixels. /// Interlaced encoding the pixels.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image.</param> /// <param name="frame">The image frame.</param>
/// <param name="deflateStream">The deflate stream.</param> /// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7Pixels<TPixel>(Image<TPixel> image, ZlibDeflateStream deflateStream) private void EncodeAdam7Pixels<TPixel>(ImageFrame<TPixel> frame, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; int width = frame.Width;
int height = image.Height; int height = frame.Height;
Buffer2D<TPixel> pixelBuffer = image.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
for (int pass = 0; pass < 7; pass++) for (int pass = 0; pass < 7; pass++)
{ {
int startRow = Adam7.FirstRow[pass]; int startRow = Adam7.FirstRow[pass];
@ -1132,7 +1199,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
col < width; col < width;
col += Adam7.ColumnIncrement[pass]) col += Adam7.ColumnIncrement[pass])
{ {
block[i++] = srcRow[col]; block[i] = srcRow[col];
++i;
} }
// Encode data // Encode data
@ -1176,7 +1244,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
stream.Write(buffer); 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) if (data.Length > 0 && length > 0)
{ {
@ -1199,7 +1267,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </returns> /// </returns>
private int CalculateScanlineLength(int width) 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 scanlineLength = width * this.bitDepth * this.bytesPerPixel;
int amount = scanlineLength % mod; int amount = scanlineLength % mod;
@ -1243,14 +1311,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (!encoder.FilterMethod.HasValue) if (!encoder.FilterMethod.HasValue)
{ {
// Specification recommends default filter method None for paletted images and Paeth for others. // Specification recommends default filter method None for paletted images and Paeth for others.
if (this.colorType == PngColorType.Palette) this.filterMethod = this.colorType is PngColorType.Palette ? PngFilterMethod.None : PngFilterMethod.Paeth;
{
this.filterMethod = PngFilterMethod.None;
}
else
{
this.filterMethod = PngFilterMethod.Paeth;
}
} }
// Ensure bit depth and color type are a supported combination. // 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; use16Bit = bits == (byte)PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit); 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; this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None;
} }
@ -1277,28 +1338,29 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="encoder">The png encoder.</param> /// <param name="encoder">The png encoder.</param>
/// <param name="colorType">The color type.</param> /// <param name="colorType">The color type.</param>
/// <param name="bitDepth">The bits per component.</param> /// <param name="bitDepth">The bits per component.</param>
/// <param name="image">The image.</param> /// <param name="frame">The frame.</param>
private static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>( private static IndexedImageFrame<TPixel>? CreateQuantizedFrame<TPixel>(
QuantizingImageEncoder encoder, QuantizingImageEncoder encoder,
PngColorType colorType, PngColorType colorType,
byte bitDepth, byte bitDepth,
Image<TPixel> image) ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (colorType != PngColorType.Palette) if (colorType is not PngColorType.Palette)
{ {
return null; return null;
} }
// Use the metadata to determine what quantization depth to use if no quantizer has been set. // Use the metadata to determine what quantization depth to use if no quantizer has been set.
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
IQuantizer quantizer = encoder.Quantizer 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. // Create quantized frame returning the palette and set the bit depth.
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(image.GetConfiguration()); using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(frame.GetConfiguration());
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image); frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); return frameQuantizer.QuantizeFrame(frame, frame.Bounds());
} }
/// <summary> /// <summary>
@ -1312,25 +1374,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private static byte CalculateBitDepth<TPixel>( private static byte CalculateBitDepth<TPixel>(
PngColorType colorType, PngColorType colorType,
byte bitDepth, byte bitDepth,
IndexedImageFrame<TPixel> quantizedFrame) IndexedImageFrame<TPixel>? quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
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); byte bits = Math.Max(bitDepth, quantizedBits);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk // 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 // 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. // be within the acceptable range.
if (bits == 3) bits = bits switch
{ {
bits = 4; 3 => 4,
} >= 5 and <= 7 => 8,
else if (bits is >= 5 and <= 7) _ => bits
{ };
bits = 8;
}
bitDepth = bits; bitDepth = bits;
} }
@ -1368,21 +1428,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The type of pixel format.</typeparam> /// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngColorType SuggestColorType<TPixel>() private static PngColorType SuggestColorType<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> typeof(TPixel) switch => default(TPixel) switch
{ {
Type t when t == typeof(A8) => PngColorType.GrayscaleWithAlpha, A8 => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Argb32) => PngColorType.RgbWithAlpha, Argb32 => PngColorType.RgbWithAlpha,
Type t when t == typeof(Bgr24) => PngColorType.Rgb, Bgr24 => PngColorType.Rgb,
Type t when t == typeof(Bgra32) => PngColorType.RgbWithAlpha, Bgra32 => PngColorType.RgbWithAlpha,
Type t when t == typeof(L8) => PngColorType.Grayscale, L8 => PngColorType.Grayscale,
Type t when t == typeof(L16) => PngColorType.Grayscale, L16 => PngColorType.Grayscale,
Type t when t == typeof(La16) => PngColorType.GrayscaleWithAlpha, La16 => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(La32) => PngColorType.GrayscaleWithAlpha, La32 => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Rgb24) => PngColorType.Rgb, Rgb24 => PngColorType.Rgb,
Type t when t == typeof(Rgba32) => PngColorType.RgbWithAlpha, Rgba32 => PngColorType.RgbWithAlpha,
Type t when t == typeof(Rgb48) => PngColorType.Rgb, Rgb48 => PngColorType.Rgb,
Type t when t == typeof(Rgba64) => PngColorType.RgbWithAlpha, Rgba64 => PngColorType.RgbWithAlpha,
Type t when t == typeof(RgbaVector) => PngColorType.RgbWithAlpha, RgbaVector => PngColorType.RgbWithAlpha,
_ => PngColorType.RgbWithAlpha _ => PngColorType.RgbWithAlpha
}; };
@ -1393,27 +1453,27 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The type of pixel format.</typeparam> /// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngBitDepth SuggestBitDepth<TPixel>() private static PngBitDepth SuggestBitDepth<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> typeof(TPixel) switch => default(TPixel) switch
{ {
Type t when t == typeof(A8) => PngBitDepth.Bit8, A8 => PngBitDepth.Bit8,
Type t when t == typeof(Argb32) => PngBitDepth.Bit8, Argb32 => PngBitDepth.Bit8,
Type t when t == typeof(Bgr24) => PngBitDepth.Bit8, Bgr24 => PngBitDepth.Bit8,
Type t when t == typeof(Bgra32) => PngBitDepth.Bit8, Bgra32 => PngBitDepth.Bit8,
Type t when t == typeof(L8) => PngBitDepth.Bit8, L8 => PngBitDepth.Bit8,
Type t when t == typeof(L16) => PngBitDepth.Bit16, L16 => PngBitDepth.Bit16,
Type t when t == typeof(La16) => PngBitDepth.Bit8, La16 => PngBitDepth.Bit8,
Type t when t == typeof(La32) => PngBitDepth.Bit16, La32 => PngBitDepth.Bit16,
Type t when t == typeof(Rgb24) => PngBitDepth.Bit8, Rgb24 => PngBitDepth.Bit8,
Type t when t == typeof(Rgba32) => PngBitDepth.Bit8, Rgba32 => PngBitDepth.Bit8,
Type t when t == typeof(Rgb48) => PngBitDepth.Bit16, Rgb48 => PngBitDepth.Bit16,
Type t when t == typeof(Rgba64) => PngBitDepth.Bit16, Rgba64 => PngBitDepth.Bit16,
Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16, RgbaVector => PngBitDepth.Bit16,
_ => PngBitDepth.Bit8 _ => PngBitDepth.Bit8
}; };
private unsafe struct ScratchBuffer private unsafe struct ScratchBuffer
{ {
private const int Size = 16; private const int Size = 26;
private fixed byte scratch[Size]; private fixed byte scratch[Size];
public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);

1
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;

22
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -7,7 +7,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
@ -107,6 +106,27 @@ public partial class PngDecoderTests
image.CompareToOriginal(provider, ImageComparer.Exact); image.CompareToOriginal(provider, ImageComparer.Exact);
} }
[Theory]
[WithFile(TestImages.Png.APng, PixelTypes.Rgba32)]
public void Decode_APng<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
image.SaveAsPng("C:\\WorkSpace\\1.png");
}
[Theory] [Theory]
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
public void PngDecoder_Decode_Resize<TPixel>(TestImageProvider<TPixel> provider) public void PngDecoder_Decode_Resize<TPixel>(TestImageProvider<TPixel> provider)

29
tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
@ -134,34 +133,6 @@ public class PngMetadataTests
VerifyExifDataIsPresent(exif); VerifyExifDataIsPresent(exif);
} }
[Theory]
[WithFile(@"C:\WorkSpace\App1\App1\Assets\7.png", PixelTypes.Rgba32)]
public void Decode_ReadsExifData2<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions options = new()
{
SkipMetadata = false
};
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions options = new()
{
SkipMetadata = false
};
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance, options);
}
[Theory] [Theory]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue<TPixel>(TestImageProvider<TPixel> provider) public void Decode_IgnoresExifData_WhenIgnoreMetadataIsTrue<TPixel>(TestImageProvider<TPixel> provider)

1
tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
namespace SixLabors.ImageSharp.Tests.Formats.Png; namespace SixLabors.ImageSharp.Tests.Formats.Png;

1
tests/ImageSharp.Tests/TestImages.cs

@ -61,6 +61,7 @@ public static class TestImages
public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31 = "Png/testpattern31x31.png";
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";
public const string XmpColorPalette = "Png/xmp-colorpalette.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 // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png"; public const string Filter0 = "Png/filter0.png";

3
tests/Images/Input/Png/apng.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6b0e5a904e269a9108b32c0f5cc98cda4240a60db421f560f45d2e36ead32a9
size 212
Loading…
Cancel
Save