// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.IO.Hashing;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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.Formats.Webp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png;
///
/// Performs the png encoding operation.
///
internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
///
/// The maximum block size, defaults at 64k for uncompressed blocks.
///
private const int MaxBlockSize = 65535;
///
/// Used the manage memory allocations.
///
private readonly MemoryAllocator memoryAllocator;
///
/// The configuration instance for the encoding operation.
///
private readonly Configuration configuration;
///
/// Reusable buffer for writing chunk data.
///
private ScratchBuffer chunkDataBuffer; // mutable struct, don't make readonly
///
/// The encoder with options
///
private readonly PngEncoder encoder;
///
/// The gamma value
///
private float? gamma;
///
/// The color type.
///
private PngColorType colorType;
///
/// The number of bits per sample or per palette index (not per pixel).
///
private byte bitDepth;
///
/// The filter method used to prefilter the encoded pixels before compression.
///
private PngFilterMethod filterMethod;
///
/// Gets the interlace mode.
///
private PngInterlaceMode interlaceMode;
///
/// The chunk filter method. This allows to filter ancillary chunks.
///
private PngChunkFilter chunkFilter;
///
/// A value indicating whether to use 16 bit encoding for supported color types.
///
private bool use16Bit;
///
/// The number of bytes per pixel.
///
private int bytesPerPixel;
///
/// The image width.
///
private int width;
///
/// The image height.
///
private int height;
///
/// The raw data of previous scanline.
///
private IMemoryOwner previousScanline = null!;
///
/// The raw data of current scanline.
///
private IMemoryOwner currentScanline = null!;
///
/// The color profile name.
///
private const string ColorProfileName = "ICC Profile";
///
/// The encoder quantizer, if present.
///
private IQuantizer? quantizer;
///
/// Any explicit quantized transparent index provided by the background color.
///
private int derivedTransparencyIndex = -1;
///
/// A reusable Crc32 hashing instance.
///
private readonly Crc32 crc32 = new();
///
/// Initializes a new instance of the class.
///
/// The configuration.
/// The encoder with options.
public PngEncoderCore(Configuration configuration, PngEncoder encoder)
{
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
this.quantizer = encoder.Quantizer;
}
///
/// Encodes the image to the specified stream from the .
///
/// The pixel format.
/// The to encode from.
/// The to encode the image data to.
/// The token to request cancellation.
public void Encode(Image image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
this.width = image.Width;
this.height = image.Height;
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = GetPngMetadata(image);
this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
stream.Write(PngConstants.HeaderBytes);
ImageFrame? clonedFrame = null;
ImageFrame currentFrame = image.Frames.RootFrame;
int currentFrameIndex = 0;
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
if (clearTransparency)
{
currentFrame = clonedFrame = currentFrame.Clone();
ClearTransparentPixels(currentFrame);
}
// Do not move this. We require an accurate bit depth for the header chunk.
IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(
pngMetadata,
currentFrame,
currentFrame.Bounds(),
null);
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
this.WriteCicpChunk(stream, metadata);
this.WriteColorProfileChunk(stream, metadata);
this.WritePaletteChunk(stream, quantized);
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
if (image.Frames.Count > 1)
{
this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount);
}
// If the first frame isn't animated, write it as usual and skip it when writing animated frames
if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1)
{
FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
currentFrameIndex++;
}
if (image.Frames.Count > 1)
{
// Write the first animated frame.
currentFrame = image.Frames[currentFrameIndex];
PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame);
PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod;
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame)
{
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
}
else
{
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
}
currentFrameIndex++;
// Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray();
// Write following frames.
ImageFrame previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size());
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
ImageFrame? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex];
ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = GetPngFrameMetadata(currentFrame);
bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
image.Configuration,
prev,
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
blend);
if (clearTransparency)
{
ClearTransparentPixels(encodingFrame);
}
// Each frame control sequence number must be incremented by the number of frame data chunks that follow.
frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
// Dispose of previous quantized frame and reassign.
quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
}
}
this.WriteEndChunk(stream);
stream.Flush();
// Dispose of allocations from final frame.
clonedFrame?.Dispose();
quantized?.Dispose();
}
///
public void Dispose()
{
this.previousScanline?.Dispose();
this.currentScanline?.Dispose();
}
private static PngMetadata GetPngMetadata(Image image)
where TPixel : unmanaged, IPixel
{
if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
{
return (PngMetadata)png.DeepClone();
}
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
{
AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata();
return PngMetadata.FromAnimatedMetadata(ani);
}
if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
{
AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata();
return PngMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
private static PngFrameMetadata GetPngFrameMetadata(ImageFrame frame)
where TPixel : unmanaged, IPixel
{
if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
{
return (PngFrameMetadata)png.DeepClone();
}
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
{
AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata();
return PngFrameMetadata.FromAnimatedMetadata(ani);
}
if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
{
AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata();
return PngFrameMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
///
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
///
/// The type of the pixel.
/// The cloned image frame where the transparent pixels will be changed.
private static void ClearTransparentPixels(ImageFrame clone)
where TPixel : unmanaged, IPixel
=> clone.ProcessPixelRows(accessor =>
{
// TODO: We should be able to speed this up with SIMD and masking.
Rgba32 transparent = Color.Transparent.ToPixel();
for (int y = 0; y < accessor.Height; y++)
{
Span span = accessor.GetRowSpan(y);
for (int x = 0; x < accessor.Width; x++)
{
ref TPixel pixel = ref span[x];
Rgba32 rgba = pixel.ToRgba32();
if (rgba.A is 0)
{
pixel = TPixel.FromRgba32(transparent);
}
}
}
});
///
/// Creates the quantized image and calculates and sets the bit depth.
///
/// The type of the pixel.
/// The image metadata.
/// The frame to quantize.
/// The area of interest within the frame.
/// Any previously derived palette.
/// The quantized image.
private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth(
PngMetadata metadata,
ImageFrame frame,
Rectangle bounds,
ReadOnlyMemory? previousPalette)
where TPixel : unmanaged, IPixel
{
IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, bounds, previousPalette);
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized;
}
/// Collects a row of grayscale pixels.
/// The pixel format.
/// The image row span.
private void CollectGrayscaleBytes(ReadOnlySpan rowSpan)
where TPixel : unmanaged, IPixel
{
Span rawScanlineSpan = this.currentScanline.GetSpan();
if (this.colorType == PngColorType.Grayscale)
{
if (this.use16Bit)
{
// 16 bit grayscale
using IMemoryOwner luminanceBuffer = this.memoryAllocator.Allocate(rowSpan.Length);
Span luminanceSpan = luminanceBuffer.GetSpan();
ref L16 luminanceRef = ref MemoryMarshal.GetReference(luminanceSpan);
PixelOperations.Instance.ToL16(this.configuration, rowSpan, luminanceSpan);
// Can't map directly to byte array as it's big-endian.
for (int x = 0, o = 0; x < luminanceSpan.Length; x++, o += 2)
{
L16 luminance = Unsafe.Add(ref luminanceRef, (uint)x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance.PackedValue);
}
}
else if (this.bitDepth == 8)
{
// 8 bit grayscale
PixelOperations.Instance.ToL8Bytes(
this.configuration,
rowSpan,
rawScanlineSpan,
rowSpan.Length);
}
else
{
// 1, 2, and 4 bit grayscale
using IMemoryOwner temp = this.memoryAllocator.Allocate(rowSpan.Length, AllocationOptions.Clean);
int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(this.bitDepth) - 1);
Span tempSpan = temp.GetSpan();
// We need to first create an array of luminance bytes then scale them down to the correct bit depth.
PixelOperations.Instance.ToL8Bytes(
this.configuration,
rowSpan,
tempSpan,
rowSpan.Length);
PngEncoderHelpers.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth, scaleFactor);
}
}
else if (this.use16Bit)
{
// 16 bit grayscale + alpha
using IMemoryOwner laBuffer = this.memoryAllocator.Allocate(rowSpan.Length);
Span laSpan = laBuffer.GetSpan();
ref La32 laRef = ref MemoryMarshal.GetReference(laSpan);
PixelOperations.Instance.ToLa32(this.configuration, rowSpan, laSpan);
// Can't map directly to byte array as it's big endian.
for (int x = 0, o = 0; x < laSpan.Length; x++, o += 4)
{
La32 la = Unsafe.Add(ref laRef, (uint)x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), la.L);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), la.A);
}
}
else
{
// 8 bit grayscale + alpha
PixelOperations.Instance.ToLa16Bytes(
this.configuration,
rowSpan,
rawScanlineSpan,
rowSpan.Length);
}
}
///
/// Collects a row of true color pixel data.
///
/// The pixel format.
/// The row span.
private void CollectTPixelBytes(ReadOnlySpan rowSpan)
where TPixel : unmanaged, IPixel
{
Span rawScanlineSpan = this.currentScanline.GetSpan();
switch (this.bytesPerPixel)
{
case 4:
// 8 bit Rgba
PixelOperations.Instance.ToRgba32Bytes(
this.configuration,
rowSpan,
rawScanlineSpan,
rowSpan.Length);
break;
case 3:
// 8 bit Rgb
PixelOperations.Instance.ToRgb24Bytes(
this.configuration,
rowSpan,
rawScanlineSpan,
rowSpan.Length);
break;
case 8:
// 16 bit Rgba
using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(rowSpan.Length))
{
Span rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba64 rgbaRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations.Instance.ToRgba64(this.configuration, rowSpan, rgbaSpan);
// Can't map directly to byte array as it's big endian.
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 8)
{
Rgba64 rgba = Unsafe.Add(ref rgbaRef, (uint)x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgba.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgba.B);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 6, 2), rgba.A);
}
}
break;
default:
// 16 bit Rgb
using (IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(rowSpan.Length))
{
Span rgbSpan = rgbBuffer.GetSpan();
ref Rgb48 rgbRef = ref MemoryMarshal.GetReference(rgbSpan);
PixelOperations.Instance.ToRgb48(this.configuration, rowSpan, rgbSpan);
// Can't map directly to byte array as it's big endian.
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 6)
{
Rgb48 rgb = Unsafe.Add(ref rgbRef, (uint)x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgb.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgb.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgb.B);
}
}
break;
}
}
///
/// Encodes the pixel data line by line.
/// Each scanline is encoded in the most optimal manner to improve compression.
///
/// The pixel format.
/// The row span.
/// The quantized pixels. Can be null.
/// The row.
private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame? quantized, int row)
where TPixel : unmanaged, IPixel
{
switch (this.colorType)
{
case PngColorType.Palette:
if (this.bitDepth < 8)
{
PngEncoderHelpers.ScaleDownFrom8BitArray(quantized!.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
quantized?.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan());
}
break;
case PngColorType.Grayscale:
case PngColorType.GrayscaleWithAlpha:
this.CollectGrayscaleBytes(rowSpan);
break;
default:
this.CollectTPixelBytes(rowSpan);
break;
}
}
///
/// Apply the line filter for the raw scanline to enable better compression.
///
/// The filtered buffer.
/// Used for attempting optimized filtering.
private void FilterPixelBytes(ref Span filter, ref Span attempt)
{
switch (this.filterMethod)
{
case PngFilterMethod.None:
NoneFilter.Encode(this.currentScanline.GetSpan(), filter);
break;
case PngFilterMethod.Sub:
SubFilter.Encode(this.currentScanline.GetSpan(), filter, this.bytesPerPixel, out int _);
break;
case PngFilterMethod.Up:
UpFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, out int _);
break;
case PngFilterMethod.Average:
AverageFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, (uint)this.bytesPerPixel, out int _);
break;
case PngFilterMethod.Paeth:
PaethFilter.Encode(this.currentScanline.GetSpan(), this.previousScanline.GetSpan(), filter, this.bytesPerPixel, out int _);
break;
default:
this.ApplyOptimalFilteredScanline(ref filter, ref attempt);
break;
}
}
///
/// Collects the pixel data line by line for compressing.
/// Each scanline is filtered in the most optimal manner to improve compression.
///
/// The pixel format.
/// The row span.
/// The filtered buffer.
/// Used for attempting optimized filtering.
/// The quantized pixels. Can be .
/// The row number.
private void CollectAndFilterPixelRow(
ReadOnlySpan rowSpan,
ref Span filter,
ref Span attempt,
IndexedImageFrame? quantized,
int row)
where TPixel : unmanaged, IPixel
{
this.CollectPixelBytes(rowSpan, quantized, row);
this.FilterPixelBytes(ref filter, ref attempt);
}
///
/// Encodes the indexed pixel data (with palette) for Adam7 interlaced mode.
///
/// The row span.
/// The filtered buffer.
/// Used for attempting optimized filtering.
private void EncodeAdam7IndexedPixelRow(
ReadOnlySpan row,
ref Span filter,
ref Span attempt)
{
// CollectPixelBytes
if (this.bitDepth < 8)
{
PngEncoderHelpers.ScaleDownFrom8BitArray(row, this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
row.CopyTo(this.currentScanline.GetSpan());
}
this.FilterPixelBytes(ref filter, ref attempt);
}
///
/// Applies all PNG filters to the given scanline and returns the filtered scanline that is deemed
/// to be most compressible, using lowest total variation as proxy for compressibility.
///
/// The filtered buffer.
/// Used for attempting optimized filtering.
private void ApplyOptimalFilteredScanline(ref Span filter, ref Span attempt)
{
// Palette images don't compress well with adaptive filtering.
// Nor do images comprising a single row.
if (this.colorType == PngColorType.Palette || this.height == 1 || this.bitDepth < 8)
{
NoneFilter.Encode(this.currentScanline.GetSpan(), filter);
return;
}
Span current = this.currentScanline.GetSpan();
Span previous = this.previousScanline.GetSpan();
int min = int.MaxValue;
SubFilter.Encode(current, attempt, this.bytesPerPixel, out int sum);
if (sum < min)
{
min = sum;
RuntimeUtility.Swap(ref filter, ref attempt);
}
UpFilter.Encode(current, previous, attempt, out sum);
if (sum < min)
{
min = sum;
RuntimeUtility.Swap(ref filter, ref attempt);
}
AverageFilter.Encode(current, previous, attempt, (uint)this.bytesPerPixel, out sum);
if (sum < min)
{
min = sum;
RuntimeUtility.Swap(ref filter, ref attempt);
}
PaethFilter.Encode(current, previous, attempt, this.bytesPerPixel, out sum);
if (sum < min)
{
RuntimeUtility.Swap(ref filter, ref attempt);
}
}
///
/// Writes the header chunk to the stream.
///
/// The containing image data.
private void WriteHeaderChunk(Stream stream)
{
PngHeader header = new(
width: this.width,
height: this.height,
bitDepth: this.bitDepth,
colorType: this.colorType,
compressionMethod: 0, // None
filterMethod: 0,
interlaceMethod: this.interlaceMode);
header.WriteTo(this.chunkDataBuffer.Span);
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size);
}
///
/// Writes the animation control chunk to the stream.
///
/// The containing image data.
/// The number of frames.
/// The number of times to loop this APNG.
private void WriteAnimationControlChunk(Stream stream, uint framesCount, uint playsCount)
{
AnimationControl acTL = new(framesCount, playsCount);
acTL.WriteTo(this.chunkDataBuffer.Span);
this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, AnimationControl.Size);
}
///
/// Writes the palette chunk to the stream.
/// Should be written before the first IDAT chunk.
///
/// The pixel format.
/// The containing image data.
/// The quantized frame.
private void WritePaletteChunk(Stream stream, IndexedImageFrame? quantized)
where TPixel : unmanaged, IPixel
{
if (quantized is null)
{
return;
}
// Grab the palette and write it to the stream.
ReadOnlySpan palette = quantized.Palette.Span;
int paletteLength = palette.Length;
int colorTableLength = paletteLength * Unsafe.SizeOf();
bool hasAlpha = false;
using IMemoryOwner colorTable = this.memoryAllocator.Allocate(colorTableLength);
using IMemoryOwner alphaTable = this.memoryAllocator.Allocate(paletteLength);
ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(colorTable.GetSpan()));
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
// Bulk convert our palette to RGBA to allow assignment to tables.
using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteLength);
Span rgbaPaletteSpan = rgbaOwner.GetSpan();
PixelOperations.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan);
ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan);
// Loop, assign, and extract alpha values from the palette.
for (int i = 0; i < paletteLength; i++)
{
Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, (uint)i);
byte alpha = rgba.A;
Unsafe.Add(ref colorTableRef, (uint)i) = rgba.Rgb;
if (alpha > this.encoder.Threshold)
{
alpha = byte.MaxValue;
}
hasAlpha = hasAlpha || alpha < byte.MaxValue;
Unsafe.Add(ref alphaTableRef, (uint)i) = alpha;
}
this.WriteChunk(stream, PngChunkType.Palette, colorTable.GetSpan(), 0, colorTableLength);
// Write the transparency data
if (hasAlpha)
{
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.GetSpan(), 0, paletteLength);
}
}
///
/// Writes the physical dimension information to the stream.
/// Should be written before IDAT chunk.
///
/// The containing image data.
/// The image metadata.
private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
{
if (this.chunkFilter.HasFlag(PngChunkFilter.ExcludePhysicalChunk))
{
return;
}
PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span);
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size);
}
///
/// Writes the eXIf chunk to the stream, if any EXIF Profile values are present in the metadata.
///
/// The containing image data.
/// The image metadata.
private void WriteExifChunk(Stream stream, ImageMetadata meta)
{
if ((this.chunkFilter & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk)
{
return;
}
if (meta.ExifProfile is null || meta.ExifProfile.Values.Count == 0)
{
return;
}
meta.SyncProfiles();
this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray());
}
///
/// Writes an iTXT chunk, containing the XMP metadata to the stream, if such profile is present in the metadata.
///
/// The containing image data.
/// The image metadata.
private void WriteXmpChunk(Stream stream, ImageMetadata meta)
{
const int iTxtHeaderSize = 5;
if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
if (meta.XmpProfile is null)
{
return;
}
byte[]? xmpData = meta.XmpProfile.Data;
if (xmpData?.Length is 0 or null)
{
return;
}
int payloadLength = xmpData.Length + PngConstants.XmpKeyword.Length + iTxtHeaderSize;
using IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength);
Span payload = owner.GetSpan();
PngConstants.XmpKeyword.CopyTo(payload);
int bytesWritten = PngConstants.XmpKeyword.Length;
// Write the iTxt header (all zeros in this case).
Span iTxtHeader = payload[bytesWritten..];
iTxtHeader[4] = 0;
iTxtHeader[3] = 0;
iTxtHeader[2] = 0;
iTxtHeader[1] = 0;
iTxtHeader[0] = 0;
bytesWritten += 5;
// And the XMP data itself.
xmpData.CopyTo(payload[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.InternationalText, payload);
}
///
/// Writes the CICP profile chunk
///
/// The containing image data.
/// The image meta data.
private void WriteCicpChunk(Stream stream, ImageMetadata metaData)
{
if (metaData.CicpProfile is null)
{
return;
}
// by spec, the matrix coefficients must be set to Identity
if (metaData.CicpProfile.MatrixCoefficients != Metadata.Profiles.Cicp.CicpMatrixCoefficients.Identity)
{
throw new NotSupportedException("CICP matrix coefficients other than Identity are not supported in PNG");
}
Span outputBytes = this.chunkDataBuffer.Span[..4];
outputBytes[0] = (byte)metaData.CicpProfile.ColorPrimaries;
outputBytes[1] = (byte)metaData.CicpProfile.TransferCharacteristics;
outputBytes[2] = (byte)metaData.CicpProfile.MatrixCoefficients;
outputBytes[3] = (byte)(metaData.CicpProfile.FullRange ? 1 : 0);
this.WriteChunk(stream, PngChunkType.Cicp, outputBytes);
}
///
/// Writes the color profile chunk.
///
/// The stream to write to.
/// The image meta data.
private void WriteColorProfileChunk(Stream stream, ImageMetadata metaData)
{
if (metaData.IccProfile is null)
{
return;
}
byte[] iccProfileBytes = metaData.IccProfile.ToByteArray();
byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes);
int payloadLength = ColorProfileName.Length + compressedData.Length + 2;
using IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength);
Span outputBytes = owner.GetSpan();
PngConstants.Encoding.GetBytes(ColorProfileName).CopyTo(outputBytes);
int bytesWritten = ColorProfileName.Length;
outputBytes[bytesWritten++] = 0; // Null separator.
outputBytes[bytesWritten++] = 0; // Compression.
compressedData.CopyTo(outputBytes[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.EmbeddedColorProfile, outputBytes);
}
///
/// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk,
/// depending whether the text contains any latin characters or should be compressed.
///
/// The containing image data.
/// The image metadata.
private void WriteTextChunks(Stream stream, PngMetadata meta)
{
if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
const int maxLatinCode = 255;
foreach (PngTextData textData in meta.TextData)
{
bool hasUnicodeCharacters = textData.Value.Any(c => c > maxLatinCode);
if (hasUnicodeCharacters || !string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))
{
// Write iTXt chunk.
byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword);
byte[] textBytes = textData.Value.Length > this.encoder.TextCompressionThreshold
? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
: PngConstants.TranslatedEncoding.GetBytes(textData.Value);
byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword);
byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag);
int payloadLength = keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5;
using IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength);
Span outputBytes = owner.GetSpan();
keywordBytes.CopyTo(outputBytes);
int bytesWritten = keywordBytes.Length;
outputBytes[bytesWritten++] = 0;
if (textData.Value.Length > this.encoder.TextCompressionThreshold)
{
// Indicate that the text is compressed.
outputBytes[bytesWritten++] = 1;
}
else
{
outputBytes[bytesWritten++] = 0;
}
outputBytes[bytesWritten++] = 0;
languageTag.CopyTo(outputBytes[bytesWritten..]);
bytesWritten += languageTag.Length;
outputBytes[bytesWritten++] = 0;
translatedKeyword.CopyTo(outputBytes[bytesWritten..]);
bytesWritten += translatedKeyword.Length;
outputBytes[bytesWritten++] = 0;
textBytes.CopyTo(outputBytes[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes);
}
else if (textData.Value.Length > this.encoder.TextCompressionThreshold)
{
// Write zTXt chunk.
byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value));
int payloadLength = textData.Keyword.Length + compressedData.Length + 2;
using IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength);
Span outputBytes = owner.GetSpan();
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
int bytesWritten = textData.Keyword.Length;
outputBytes[bytesWritten++] = 0; // Null separator.
outputBytes[bytesWritten++] = 0; // Compression.
compressedData.CopyTo(outputBytes[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes);
}
else
{
// Write tEXt chunk.
int payloadLength = textData.Keyword.Length + textData.Value.Length + 1;
using IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength);
Span outputBytes = owner.GetSpan();
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
int bytesWritten = textData.Keyword.Length;
outputBytes[bytesWritten++] = 0;
PngConstants.Encoding.GetBytes(textData.Value).CopyTo(outputBytes[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.Text, outputBytes);
}
}
}
///
/// Compresses a given text using Zlib compression.
///
/// The bytes to compress.
/// The compressed byte array.
private byte[] GetZlibCompressedBytes(byte[] dataBytes)
{
using MemoryStream memoryStream = new();
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel))
{
deflateStream.Write(dataBytes);
}
return memoryStream.ToArray();
}
///
/// Writes the gamma information to the stream.
/// Should be written before PLTE and IDAT chunk.
///
/// The containing image data.
private void WriteGammaChunk(Stream stream)
{
if ((this.chunkFilter & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk)
{
return;
}
if (this.gamma > 0)
{
// 4-byte unsigned integer of gamma * 100,000.
uint gammaValue = (uint)(this.gamma * 100_000F);
BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue);
this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4);
}
}
///
/// Writes the transparency chunk to the stream.
/// Should be written after PLTE and before IDAT.
///
/// The containing image data.
/// The image metadata.
private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
{
if (pngMetadata.TransparentColor is null)
{
return;
}
Span alpha = this.chunkDataBuffer.Span;
if (pngMetadata.ColorType == PngColorType.Rgb)
{
if (this.use16Bit)
{
Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel();
BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
}
else
{
alpha.Clear();
Rgb24 rgb = pngMetadata.TransparentColor.Value.ToPixel();
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 (this.use16Bit)
{
L16 l16 = pngMetadata.TransparentColor.Value.ToPixel();
BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
}
else
{
L8 l8 = pngMetadata.TransparentColor.Value.ToPixel();
alpha.Clear();
alpha[1] = l8.PackedValue;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
}
}
}
///
/// Writes the animation control chunk to the stream.
///
/// The containing image data.
/// The frame metadata.
/// The frame area of interest.
/// The frame sequence number.
private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, Rectangle bounds, uint sequenceNumber)
{
FrameControl fcTL = new(
sequenceNumber: sequenceNumber,
width: (uint)bounds.Width,
height: (uint)bounds.Height,
xOffset: (uint)bounds.Left,
yOffset: (uint)bounds.Top,
delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator,
delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator,
disposeOperation: frameMetadata.DisposalMethod,
blendOperation: frameMetadata.BlendMethod);
fcTL.WriteTo(this.chunkDataBuffer.Span);
this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, FrameControl.Size);
return fcTL;
}
///
/// Writes the pixel information to the stream.
///
/// The pixel format.
/// The frame control
/// The image frame.
/// The quantized pixel data. Can be null.
/// The stream.
/// Is writing fdAT or IDAT.
private uint WriteDataChunks(FrameControl frameControl, Buffer2DRegion frame, IndexedImageFrame? quantized, Stream stream, bool isFrame)
where TPixel : unmanaged, IPixel
{
byte[] buffer;
int bufferLength;
using (MemoryStream memoryStream = new())
{
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel))
{
if (this.interlaceMode is PngInterlaceMode.Adam7)
{
if (quantized is not null)
{
this.EncodeAdam7IndexedPixels(quantized, deflateStream);
}
else
{
this.EncodeAdam7Pixels(frame, deflateStream);
}
}
else
{
this.EncodePixels(frame, quantized, deflateStream);
}
}
buffer = memoryStream.ToArray();
bufferLength = buffer.Length;
}
// Store the chunks in repeated 64k blocks.
// This reduces the memory load for decoding the image for many decoders.
int maxBlockSize = MaxBlockSize;
if (isFrame)
{
maxBlockSize -= 4;
}
int numChunks = bufferLength / maxBlockSize;
if (bufferLength % maxBlockSize != 0)
{
numChunks++;
}
for (int i = 0; i < numChunks; i++)
{
int length = bufferLength - (i * maxBlockSize);
if (length > maxBlockSize)
{
length = maxBlockSize;
}
if (isFrame)
{
// We increment the sequence number for each frame chunk.
// '1' is added to the sequence number to account for the preceding frame control chunk.
uint sequenceNumber = (uint)(frameControl.SequenceNumber + 1 + i);
this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length);
}
else
{
this.WriteChunk(stream, PngChunkType.Data, buffer, i * maxBlockSize, length);
}
}
return (uint)numChunks;
}
///
/// Allocates the buffers for each scanline.
///
/// The bytes per scanline.
private void AllocateScanlineBuffers(int bytesPerScanline)
{
// Clean up from any potential previous runs.
this.previousScanline?.Dispose();
this.currentScanline?.Dispose();
this.previousScanline = this.memoryAllocator.Allocate(bytesPerScanline, AllocationOptions.Clean);
this.currentScanline = this.memoryAllocator.Allocate(bytesPerScanline, AllocationOptions.Clean);
}
///
/// Encodes the pixels.
///
/// The type of the pixel.
/// The image frame pixel buffer.
/// The quantized pixels.
/// The deflate stream.
private void EncodePixels(Buffer2DRegion pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
int bytesPerScanline = this.CalculateScanlineLength(pixels.Width);
int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline);
using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
Span filter = filterBuffer.GetSpan();
Span attempt = attemptBuffer.GetSpan();
for (int y = 0; y < pixels.Height; y++)
{
this.CollectAndFilterPixelRow(pixels.DangerousGetRowSpan(y), ref filter, ref attempt, quantized, y);
deflateStream.Write(filter);
this.SwapScanlineBuffers();
}
}
///
/// Interlaced encoding the pixels.
///
/// The type of the pixel.
/// The image frame pixel buffer.
/// The deflate stream.
private void EncodeAdam7Pixels(Buffer2DRegion pixels, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
for (int pass = 0; pass < 7; pass++)
{
int startRow = Adam7.FirstRow[pass];
int startCol = Adam7.FirstColumn[pass];
int blockWidth = Adam7.ComputeBlockWidth(pixels.Width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1
? ((blockWidth * this.bitDepth) + 7) / 8
: blockWidth * this.bytesPerPixel;
int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline);
using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate(blockWidth);
using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
Span block = blockBuffer.GetSpan();
Span filter = filterBuffer.GetSpan();
Span attempt = attemptBuffer.GetSpan();
for (int row = startRow; row < pixels.Height; row += Adam7.RowIncrement[pass])
{
// Collect pixel data
Span srcRow = pixels.DangerousGetRowSpan(row);
for (int col = startCol, i = 0; col < pixels.Width; col += Adam7.ColumnIncrement[pass], i++)
{
block[i] = srcRow[col];
}
// Encode data
// Note: quantized parameter not used
// Note: row parameter not used
this.CollectAndFilterPixelRow(block, ref filter, ref attempt, null, -1);
deflateStream.Write(filter);
this.SwapScanlineBuffers();
}
}
}
///
/// Interlaced encoding the quantized (indexed, with palette) pixels.
///
/// The type of the pixel.
/// The quantized.
/// The deflate stream.
private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
for (int pass = 0; pass < 7; pass++)
{
int startRow = Adam7.FirstRow[pass];
int startCol = Adam7.FirstColumn[pass];
int blockWidth = Adam7.ComputeBlockWidth(quantized.Width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1
? ((blockWidth * this.bitDepth) + 7) / 8
: blockWidth * this.bytesPerPixel;
int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline);
using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate(blockWidth);
using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
Span block = blockBuffer.GetSpan();
Span filter = filterBuffer.GetSpan();
Span attempt = attemptBuffer.GetSpan();
for (int row = startRow; row < quantized.Height; row += Adam7.RowIncrement[pass])
{
// Collect data
ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row);
for (int col = startCol, i = 0; col < quantized.Width; col += Adam7.ColumnIncrement[pass], i++)
{
block[i] = srcRow[col];
}
// Encode data
this.EncodeAdam7IndexedPixelRow(block, ref filter, ref attempt);
deflateStream.Write(filter);
this.SwapScanlineBuffers();
}
}
}
///
/// Writes the chunk end to the stream.
///
/// The containing image data.
private void WriteEndChunk(Stream stream) => this.WriteChunk(stream, PngChunkType.End, null);
///
/// Writes a chunk to the stream.
///
/// The to write to.
/// The type of chunk to write.
/// The containing data.
private void WriteChunk(Stream stream, PngChunkType type, Span data)
=> this.WriteChunk(stream, type, data, 0, data.Length);
///
/// Writes a chunk of a specified length to the stream at the given offset.
///
/// The to write to.
/// The type of chunk to write.
/// The containing data.
/// The position to offset the data at.
/// The of the data to write.
private void WriteChunk(Stream stream, PngChunkType type, Span data, int offset, int length)
{
Span buffer = stackalloc byte[8];
BinaryPrimitives.WriteInt32BigEndian(buffer, length);
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), (uint)type);
stream.Write(buffer);
this.crc32.Reset();
this.crc32.Append(buffer[4..]); // Write the type buffer
if (data.Length > 0 && length > 0)
{
stream.Write(data, offset, length);
this.crc32.Append(data.Slice(offset, length));
}
BinaryPrimitives.WriteUInt32BigEndian(buffer, this.crc32.GetCurrentHashAsUInt32());
stream.Write(buffer, 0, 4); // write the crc
}
///
/// Writes a frame data chunk of a specified length to the stream at the given offset.
///
/// The to write to.
/// The frame sequence number.
/// The containing data.
/// The position to offset the data at.
/// The of the data to write.
private void WriteFrameDataChunk(Stream stream, uint sequenceNumber, Span data, int offset, int length)
{
Span buffer = stackalloc byte[12];
BinaryPrimitives.WriteInt32BigEndian(buffer, length + 4);
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), (uint)PngChunkType.FrameData);
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(8, 4), sequenceNumber);
stream.Write(buffer);
this.crc32.Reset();
this.crc32.Append(buffer[4..]); // Write the type buffer
if (data.Length > 0 && length > 0)
{
stream.Write(data, offset, length);
this.crc32.Append(data.Slice(offset, length));
}
BinaryPrimitives.WriteUInt32BigEndian(buffer, this.crc32.GetCurrentHashAsUInt32());
stream.Write(buffer, 0, 4); // write the crc
}
///
/// Calculates the scanline length.
///
/// The width of the row.
///
/// The representing the length.
///
private int CalculateScanlineLength(int width)
{
int mod = this.bitDepth is 16 ? 16 : 8;
int scanlineLength = width * this.bitDepth * this.bytesPerPixel;
int amount = scanlineLength % mod;
if (amount != 0)
{
scanlineLength += mod - amount;
}
return scanlineLength / mod;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SwapScanlineBuffers()
{
ref IMemoryOwner prev = ref this.previousScanline;
ref IMemoryOwner current = ref this.currentScanline;
RuntimeUtility.Swap(ref prev, ref current);
}
///
/// Adjusts the options based upon the given metadata.
///
/// The type of pixel format.
/// The encoder with options.
/// The PNG metadata.
/// if set to true [use16 bit].
/// The bytes per pixel.
private void SanitizeAndSetEncoderOptions(
PngEncoder encoder,
PngMetadata pngMetadata,
out bool use16Bit,
out int bytesPerPixel)
where TPixel : unmanaged, IPixel
{
// Always take the encoder options over the metadata values.
this.gamma = encoder.Gamma ?? pngMetadata.Gamma;
// Use options, then check metadata, if nothing set there then we suggest
// a sensible default based upon the pixel format.
PngColorType? colorType = encoder.ColorType ?? pngMetadata.ColorType;
byte? bits = (byte?)(encoder.BitDepth ?? pngMetadata.BitDepth);
if (colorType is null || bits is null)
{
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
PixelComponentInfo? componentInfo = info.ComponentInfo;
colorType ??= SuggestColorType(in info);
if (bits is null)
{
// TODO: Update once we stop abusing PixelTypeInfo in decoders.
if (componentInfo.HasValue)
{
PixelComponentInfo c = componentInfo.Value;
bits = (byte)SuggestBitDepth(in c);
}
else
{
bits = (byte)PngBitDepth.Bit8;
}
}
}
// Ensure bit depth and color type are a supported combination.
// Bit8 is the only bit depth supported by all color types.
byte[] validBitDepths = PngConstants.ColorTypes[colorType.Value];
if (Array.IndexOf(validBitDepths, bits) == -1)
{
bits = (byte)PngBitDepth.Bit8;
}
this.colorType = colorType.Value;
this.bitDepth = bits.Value;
if (encoder.FilterMethod.HasValue)
{
this.filterMethod = encoder.FilterMethod.Value;
}
else
{
// Specification recommends default filter method None for paletted images and Paeth for others.
this.filterMethod = this.colorType is PngColorType.Palette ? PngFilterMethod.None : PngFilterMethod.Paeth;
}
use16Bit = bits == (byte)PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit);
this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod)!.Value;
this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None;
}
///
/// Creates the quantized frame.
///
/// The type of the pixel.
/// The png encoder.
/// The color type.
/// The bits per component.
/// The image metadata.
/// The frame to quantize.
/// The frame area of interest.
/// Any previously derived palette.
private IndexedImageFrame? CreateQuantizedFrame(
QuantizingImageEncoder encoder,
PngColorType colorType,
byte bitDepth,
PngMetadata metadata,
ImageFrame frame,
Rectangle bounds,
ReadOnlyMemory? previousPalette)
where TPixel : unmanaged, IPixel
{
if (colorType is not PngColorType.Palette)
{
return null;
}
if (previousPalette is not null)
{
// Use the previously derived palette created by quantizing the root frame to quantize the current frame.
using PaletteQuantizer paletteQuantizer = new(
this.configuration,
this.quantizer!.Options,
previousPalette.Value,
this.derivedTransparencyIndex);
paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return paletteQuantizer.QuantizeFrame(frame, bounds);
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
if (this.quantizer is null)
{
if (metadata.ColorTable is not null)
{
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
ReadOnlySpan palette = metadata.ColorTable.Value.Span;
// Certain operations perform alpha premultiplication, which can cause the color to change so we
// must search for the transparency index in the palette.
// Transparent pixels are much more likely to be found at the end of a palette.
int index = -1;
for (int i = palette.Length - 1; i >= 0; i--)
{
Vector4 instance = palette[i].ToScaledVector4();
if (instance.W == 0f)
{
index = i;
break;
}
}
this.derivedTransparencyIndex = index;
this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null }, this.derivedTransparencyIndex);
}
else
{
this.quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
}
}
// Create quantized frame returning the palette and set the bit depth.
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration);
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return frameQuantizer.QuantizeFrame(frame, bounds);
}
///
/// Calculates the bit depth value.
///
/// The type of the pixel.
/// The color type.
/// The bits per component.
/// The quantized frame.
/// Bit depth is not supported or not valid.
private static byte CalculateBitDepth(
PngColorType colorType,
byte bitDepth,
IndexedImageFrame? quantizedFrame)
where TPixel : unmanaged, IPixel
{
if (colorType is PngColorType.Palette)
{
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.
bits = bits switch
{
3 => 4,
>= 5 and <= 7 => 8,
_ => bits
};
bitDepth = bits;
}
if (Array.IndexOf(PngConstants.ColorTypes[colorType], bitDepth) < 0)
{
throw new NotSupportedException("Bit depth is not supported or not valid.");
}
return bitDepth;
}
///
/// Calculates the correct number of bytes per pixel for the given color type.
///
/// The color type.
/// Whether to use 16 bits per component.
/// Bytes per pixel.
private static int CalculateBytesPerPixel(PngColorType? pngColorType, bool use16Bit)
=> pngColorType switch
{
PngColorType.Grayscale => use16Bit ? 2 : 1,
PngColorType.GrayscaleWithAlpha => use16Bit ? 4 : 2,
PngColorType.Palette => 1,
PngColorType.Rgb => use16Bit ? 6 : 3,
// PngColorType.RgbWithAlpha
_ => use16Bit ? 8 : 4,
};
///
/// Returns a suggested for the given
///
/// The pixel type info.
/// The type of pixel format.
private static PngColorType SuggestColorType(in PixelTypeInfo info)
where TPixel : unmanaged, IPixel
{
if (info.AlphaRepresentation == PixelAlphaRepresentation.None)
{
return info.ColorType switch
{
PixelColorType.Grayscale => PngColorType.Grayscale,
_ => PngColorType.Rgb,
};
}
return info.ColorType switch
{
PixelColorType.Grayscale | PixelColorType.Alpha or PixelColorType.Alpha => PngColorType.GrayscaleWithAlpha,
_ => PngColorType.RgbWithAlpha,
};
}
///
/// Returns a suggested for the given
///
/// The pixel type info.
/// The type of pixel format.
private static PngBitDepth SuggestBitDepth(in PixelComponentInfo info)
where TPixel : unmanaged, IPixel
{
int bits = info.GetMaximumComponentPrecision();
if (bits > (int)PixelComponentBitDepth.Bit8)
{
return PngBitDepth.Bit16;
}
return PngBitDepth.Bit8;
}
private unsafe struct ScratchBuffer
{
private const int Size = 26;
private fixed byte scratch[Size];
public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
}
}