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.
/// </summary>
/// <param name="configurationModules">A collection of configuration modules to register.</param>
public Configuration(params IImageFormatConfigurationModule[] configurationModules)
public Configuration(params IImageFormatConfigurationModule[]? configurationModules)
{
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>
/// Writes the fcTL to the given buffer.
/// </summary>
@ -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;

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

@ -10,31 +10,31 @@ internal enum PngChunkType : uint
{
/// <summary>
/// </summary>
/// <remarks>acTL</remarks>
/// <remarks>acTL (Single)</remarks>
AnimationControl = 0x6163544cU,
/// <summary>
/// </summary>
/// <remarks>fcTL</remarks>
/// <remarks>fcTL (Multiple)</remarks>
FrameControl = 0x6663544cU,
/// <summary>
/// </summary>
/// <remarks>fdAT</remarks>
/// <remarks>fdAT (Multiple)</remarks>
FrameData = 0x66644154U,
/// <summary>
/// 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.
/// </summary>
/// <remarks>IDAT</remarks>
/// <remarks>IDAT (Multiple)</remarks>
Data = 0x49444154U,
/// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
/// </summary>
/// <remarks>IEND</remarks>
/// <remarks>IEND (Single)</remarks>
End = 0x49454E44U,
/// <summary>
@ -42,40 +42,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or
/// the used compression method.
/// </summary>
/// <remarks>IHDR</remarks>
/// <remarks>IHDR (Single)</remarks>
Header = 0x49484452U,
/// <summary>
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
/// </summary>
/// <remarks>PLTE</remarks>
/// <remarks>PLTE (Single)</remarks>
Palette = 0x504C5445U,
/// <summary>
/// The eXIf data chunk which contains the Exif profile.
/// </summary>
/// <remarks>eXIF</remarks>
/// <remarks>eXIF (Single)</remarks>
Exif = 0x65584966U,
/// <summary>
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
/// </summary>
/// <remarks>gAMA</remarks>
/// <remarks>gAMA (Single)</remarks>
Gamma = 0x67414D41U,
/// <summary>
/// This chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary>
/// <remarks>pHYs</remarks>
/// <remarks>pHYs (Single)</remarks>
Physical = 0x70485973U,
/// <summary>
/// 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.
/// </summary>
/// <remarks>tEXT</remarks>
/// <remarks>tEXT (Multiple)</remarks>
Text = 0x74455874U,
/// <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
/// a compressed text string.
/// </summary>
/// <remarks>zTXt</remarks>
/// <remarks>zTXt (Multiple)</remarks>
CompressedText = 0x7A545874U,
/// <summary>
/// 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.
/// </summary>
/// <remarks>iTXt</remarks>
/// <remarks>iTXt (Multiple)</remarks>
InternationalText = 0x69545874U,
/// <summary>
@ -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).
/// </summary>
/// <remarks>tRNS</remarks>
/// <remarks>tRNS (Single)</remarks>
Transparency = 0x74524E53U,
/// <summary>
/// This chunk gives the time of the last image modification (not the time of initial image creation).
/// </summary>
/// <remarks>tIME</remarks>
/// <remarks>tIME (Single)</remarks>
Time = 0x74494d45,
/// <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),
/// the bKGD chunk should be ignored.
/// </summary>
/// <remarks>bKGD</remarks>
/// <remarks>bKGD (Single)</remarks>
Background = 0x624b4744,
/// <summary>
/// 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.
/// </summary>
/// <remarks>iCCP</remarks>
/// <remarks>iCCP (Single)</remarks>
EmbeddedColorProfile = 0x69434350,
/// <summary>
/// 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.
/// </summary>
/// <remarks>sBIT</remarks>
/// <remarks>sBIT (Single)</remarks>
SignificantBits = 0x73424954,
/// <summary>
/// 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.
/// </summary>
/// <remarks>sRGB</remarks>
/// <remarks>sRGB (Single)</remarks>
StandardRgbColourSpace = 0x73524742,
/// <summary>
/// This chunk gives the approximate usage frequency of each colour in the palette.
/// </summary>
/// <remarks>hIST</remarks>
/// <remarks>hIST (Single)</remarks>
Histogram = 0x68495354,
/// <summary>
/// This chunk contains the suggested palette.
/// </summary>
/// <remarks>sPLT</remarks>
/// <remarks>sPLT (Single)</remarks>
SuggestedPalette = 0x73504c54,
/// <summary>
/// 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.
/// </summary>
/// <remarks>cHRM</remarks>
/// <remarks>cHRM (Single)</remarks>
Chroma = 0x6348524d,
/// <summary>

8
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!;
/// <summary>
/// Gets whether the file is a simple PNG.
/// </summary>
public bool? IsSimplePng { get; init; }
/// <summary>
/// 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.
// 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
/// <summary>
/// The maximum block size, defaults at 64k for uncompressed blocks.
/// </summary>
private const int MaxBlockSize = 65535;
private const int MaxBlockSize = (1 << 16) - 1;
/// <summary>
/// Used the manage memory allocations.
@ -102,12 +100,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <summary>
/// The raw data of previous scanline.
/// </summary>
private IMemoryOwner<byte> previousScanline;
private IMemoryOwner<byte> previousScanline = null!;
/// <summary>
/// The raw data of current scanline.
/// </summary>
private IMemoryOwner<byte> currentScanline;
private IMemoryOwner<byte> currentScanline = null!;
/// <summary>
/// The color profile name.
@ -147,34 +145,59 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
Image<TPixel> clonedImage = null;
bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear;
Image<TPixel>? clonedImage = null;
Image<TPixel> targetImage = image;
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
if (clearTransparency)
{
clonedImage = image.Clone();
ClearTransparentPixels(clonedImage);
targetImage = clonedImage = image.Clone();
ClearTransparentPixels(targetImage);
}
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage);
IndexedImageFrame<TPixel>? 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<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);
stream.Flush();
quantized?.Dispose();
clonedImage?.Dispose();
rootQuantized?.Dispose();
}
/// <inheritdoc />
@ -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!;
}
/// <summary>
@ -192,48 +215,44 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The cloned image where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> =>
image.ProcessPixelRows(accessor =>
where TPixel : unmanaged, IPixel<TPixel>
{
foreach (ImageFrame<TPixel> 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<TPixel> 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<TPixel> 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);
}
}
}
}
});
});
}
}
/// <summary>
/// Creates the quantized image and calculates and sets the bit depth.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image to quantize.</param>
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param>
/// <param name="frame">The frame to quantize.</param>
/// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel> CreateQuantizedImageAndUpdateBitDepth<TPixel>(
Image<TPixel> image,
Image<TPixel> clonedImage)
private IndexedImageFrame<TPixel>? CreateQuantizedImageAndUpdateBitDepth<TPixel>(
ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel> 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<TPixel>? 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<TPixel>(ReadOnlySpan<TPixel> rowSpan)
where TPixel : unmanaged, IPixel<TPixel>
{
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span<byte> 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<TPixel>.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<TPixel>.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
/// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</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>
{
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<TPixel> rowSpan,
ref Span<byte> filter,
ref Span<byte> attempt,
IndexedImageFrame<TPixel> quantized,
IndexedImageFrame<TPixel>? quantized,
int row)
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);
}
/// <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>
/// 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
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</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>
{
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<byte> 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;
}
}
/// <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>
/// Writes the pixel information to the stream.
/// </summary>
/// <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="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>
{
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;
}
/// <summary>
@ -1013,10 +1072,18 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</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>
{
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<byte> filter = filterBuffer.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);
deflateStream.Write(filter);
@ -1040,14 +1107,14 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Interlaced encoding the pixels.
/// </summary>
/// <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>
private void EncodeAdam7Pixels<TPixel>(Image<TPixel> image, ZlibDeflateStream deflateStream)
private void EncodeAdam7Pixels<TPixel>(ImageFrame<TPixel> frame, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
Buffer2D<TPixel> pixelBuffer = image.Frames.RootFrame.PixelBuffer;
int width = frame.Width;
int height = frame.Height;
Buffer2D<TPixel> 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
/// </returns>
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
/// <param name="encoder">The png encoder.</param>
/// <param name="colorType">The color type.</param>
/// <param name="bitDepth">The bits per component.</param>
/// <param name="image">The image.</param>
private static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
/// <param name="frame">The frame.</param>
private static IndexedImageFrame<TPixel>? CreateQuantizedFrame<TPixel>(
QuantizingImageEncoder encoder,
PngColorType colorType,
byte bitDepth,
Image<TPixel> image)
ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(image.GetConfiguration());
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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());
}
/// <summary>
@ -1312,25 +1374,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private static byte CalculateBitDepth<TPixel>(
PngColorType colorType,
byte bitDepth,
IndexedImageFrame<TPixel> quantizedFrame)
IndexedImageFrame<TPixel>? quantizedFrame)
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);
// 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
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngColorType SuggestColorType<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
=> 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
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngBitDepth SuggestBitDepth<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
=> 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<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);

1
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;

22
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<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]
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
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.
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<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]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
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.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
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 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";

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