// 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.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; 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 : 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 = metadata.ClonePngMetadata(); 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 = currentFrame.Metadata.GetPngMetadata(); FrameDisposalMode previousDisposal = frameMetadata.DisposalMode; 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 == FrameDisposalMode.RestoreToBackground ? null : previousFrame; currentFrame = image.Frames[currentFrameIndex]; ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; frameMetadata = currentFrame.Metadata.GetPngMetadata(); bool blend = frameMetadata.BlendMode == FrameBlendMode.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.DisposalMode; } } this.WriteEndChunk(stream); stream.Flush(); // Dispose of allocations from final frame. clonedFrame?.Dispose(); quantized?.Dispose(); } /// public void Dispose() { this.previousScanline?.Dispose(); this.currentScanline?.Dispose(); } /// /// 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; } 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. /// CICP matrix coefficients other than Identity are not supported in PNG. 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, disposalMode: frameMetadata.DisposalMode, blendMode: frameMetadata.BlendMode); 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 color = encoder.ColorType ?? pngMetadata.ColorType; byte bits = (byte)(encoder.BitDepth ?? pngMetadata.BitDepth); // Ensure the bit depth and color type are a supported combination. // Bit8 is the only bit depth supported by all color types. byte[] validBitDepths = PngConstants.ColorTypes[color]; if (Array.IndexOf(validBitDepths, bits) == -1) { bits = (byte)PngBitDepth.Bit8; } this.colorType = color; this.bitDepth = bits; 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; 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, }; private unsafe struct ScratchBuffer { private const int Size = 26; private fixed byte scratch[Size]; public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); } }