From aada974cdb883a84fe69eb52e98a496c06bdd0cb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 Oct 2023 22:13:05 +1000 Subject: [PATCH] Refactor and cleanup --- .../Formats/Png/Chunks/FrameControl.cs | 119 ++++----- ...PngBlendOperation.cs => PngBlendMethod.cs} | 4 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 57 ++-- ...sposeOperation.cs => PngDisposalMethod.cs} | 4 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 248 +++++++++++------- .../Formats/Png/PngFrameMetadata.cs | 48 +--- .../Formats/Png/PngScanlineProcessor.cs | 176 +++++-------- 7 files changed, 309 insertions(+), 347 deletions(-) rename src/ImageSharp/Formats/Png/{PngBlendOperation.cs => PngBlendMethod.cs} (93%) rename src/ImageSharp/Formats/Png/{PngDisposeOperation.cs => PngDisposalMethod.cs} (92%) diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index bb75cbabf..c7233ada1 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -10,22 +10,22 @@ internal readonly struct FrameControl public const int Size = 26; public FrameControl( - int sequenceNumber, - int width, - int height, - int xOffset, - int yOffset, - short delayNumber, - short delayDenominator, - PngDisposeOperation disposeOperation, - PngBlendOperation blendOperation) + uint sequenceNumber, + uint width, + uint height, + uint xOffset, + uint yOffset, + ushort delayNumerator, + ushort delayDenominator, + PngDisposalMethod disposeOperation, + PngBlendMethod blendOperation) { this.SequenceNumber = sequenceNumber; this.Width = width; this.Height = height; this.XOffset = xOffset; this.YOffset = yOffset; - this.DelayNumber = delayNumber; + this.DelayNumerator = delayNumerator; this.DelayDenominator = delayDenominator; this.DisposeOperation = disposeOperation; this.BlendOperation = blendOperation; @@ -34,130 +34,101 @@ internal readonly struct FrameControl /// /// Gets the sequence number of the animation chunk, starting from 0 /// - public int SequenceNumber { get; } + public uint SequenceNumber { get; } /// /// Gets the width of the following frame /// - public int Width { get; } + public uint Width { get; } /// /// Gets the height of the following frame /// - public int Height { get; } + public uint Height { get; } /// /// Gets the X position at which to render the following frame /// - public int XOffset { get; } + public uint XOffset { get; } /// /// Gets the Y position at which to render the following frame /// - public int YOffset { get; } + public uint YOffset { get; } /// /// Gets the X limit at which to render the following frame /// - public uint XLimit => (uint)(this.XOffset + this.Width); + public uint XMax => this.XOffset + this.Width; /// /// Gets the Y limit at which to render the following frame /// - public uint YLimit => (uint)(this.YOffset + this.Height); + public uint YMax => this.YOffset + this.Height; /// /// Gets the frame delay fraction numerator /// - public short DelayNumber { get; } + public ushort DelayNumerator { get; } /// /// Gets the frame delay fraction denominator /// - public short DelayDenominator { get; } + public ushort DelayDenominator { get; } /// /// Gets the type of frame area disposal to be done after rendering this frame /// - public PngDisposeOperation DisposeOperation { get; } + public PngDisposalMethod DisposeOperation { get; } /// /// Gets the type of frame area rendering for this frame /// - public PngBlendOperation BlendOperation { get; } + public PngBlendMethod BlendOperation { get; } /// /// Validates the APng fcTL. /// + /// The header. /// /// Thrown if the image does pass validation. /// - public void Validate(PngHeader hdr) + public void Validate(PngHeader header) { - if (this.XOffset < 0) - { - PngThrowHelper.ThrowInvalidParameter(this.XOffset, "Expected >= 0"); - } - - if (this.YOffset < 0) - { - PngThrowHelper.ThrowInvalidParameter(this.YOffset, "Expected >= 0"); - } - - if (this.Width <= 0) + if (this.Width == 0) { PngThrowHelper.ThrowInvalidParameter(this.Width, "Expected > 0"); } - if (this.Height <= 0) + if (this.Height == 0) { PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0"); } - if (this.XLimit > hdr.Width) + if (this.XMax > header.Width) { - PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Width)}"); + PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The x-offset plus width > {nameof(PngHeader)}.{nameof(PngHeader.Width)}"); } - if (this.YLimit > hdr.Height) + if (this.YMax > header.Height) { - PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Height)}"); + PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The y-offset plus height > {nameof(PngHeader)}.{nameof(PngHeader.Height)}"); } } - /// - /// Parses the APngFrameControl from the given metadata. - /// - /// The metadata to parse. - /// Sequence number. - public static FrameControl FromMetadata(PngFrameMetadata frameMetadata, int sequenceNumber) - { - FrameControl fcTL = new( - sequenceNumber, - frameMetadata.Width, - frameMetadata.Height, - frameMetadata.XOffset, - frameMetadata.YOffset, - frameMetadata.DelayNumber, - frameMetadata.DelayDenominator, - frameMetadata.DisposeOperation, - frameMetadata.BlendOperation); - return fcTL; - } - /// /// Writes the fcTL to the given buffer. /// /// The buffer to write to. public void WriteTo(Span buffer) { - BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.SequenceNumber); - BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.Width); - BinaryPrimitives.WriteInt32BigEndian(buffer[8..12], this.Height); - BinaryPrimitives.WriteInt32BigEndian(buffer[12..16], this.XOffset); - BinaryPrimitives.WriteInt32BigEndian(buffer[16..20], this.YOffset); - BinaryPrimitives.WriteInt16BigEndian(buffer[20..22], this.DelayNumber); - BinaryPrimitives.WriteInt16BigEndian(buffer[22..24], this.DelayDenominator); + BinaryPrimitives.WriteUInt32BigEndian(buffer[..4], this.SequenceNumber); + BinaryPrimitives.WriteUInt32BigEndian(buffer[4..8], this.Width); + BinaryPrimitives.WriteUInt32BigEndian(buffer[8..12], this.Height); + BinaryPrimitives.WriteUInt32BigEndian(buffer[12..16], this.XOffset); + BinaryPrimitives.WriteUInt32BigEndian(buffer[16..20], this.YOffset); + BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator); + BinaryPrimitives.WriteUInt16BigEndian(buffer[22..24], this.DelayDenominator); buffer[24] = (byte)this.DisposeOperation; buffer[25] = (byte)this.BlendOperation; @@ -170,13 +141,13 @@ internal readonly struct FrameControl /// The parsed fcTL. public static FrameControl Parse(ReadOnlySpan data) => new( - sequenceNumber: BinaryPrimitives.ReadInt32BigEndian(data[..4]), - width: BinaryPrimitives.ReadInt32BigEndian(data[4..8]), - height: BinaryPrimitives.ReadInt32BigEndian(data[8..12]), - xOffset: BinaryPrimitives.ReadInt32BigEndian(data[12..16]), - yOffset: BinaryPrimitives.ReadInt32BigEndian(data[16..20]), - delayNumber: BinaryPrimitives.ReadInt16BigEndian(data[20..22]), - delayDenominator: BinaryPrimitives.ReadInt16BigEndian(data[22..24]), - disposeOperation: (PngDisposeOperation)data[24], - blendOperation: (PngBlendOperation)data[25]); + sequenceNumber: BinaryPrimitives.ReadUInt32BigEndian(data[..4]), + width: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]), + height: BinaryPrimitives.ReadUInt32BigEndian(data[8..12]), + xOffset: BinaryPrimitives.ReadUInt32BigEndian(data[12..16]), + yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]), + delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]), + delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]), + disposeOperation: (PngDisposalMethod)data[24], + blendOperation: (PngBlendMethod)data[25]); } diff --git a/src/ImageSharp/Formats/Png/PngBlendOperation.cs b/src/ImageSharp/Formats/Png/PngBlendMethod.cs similarity index 93% rename from src/ImageSharp/Formats/Png/PngBlendOperation.cs rename to src/ImageSharp/Formats/Png/PngBlendMethod.cs index b8a84a933..b7ace9ccf 100644 --- a/src/ImageSharp/Formats/Png/PngBlendOperation.cs +++ b/src/ImageSharp/Formats/Png/PngBlendMethod.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats.Png; @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Specifies whether the frame is to be alpha blended into the current output buffer content, or whether it should completely replace its region in the output buffer. /// -public enum PngBlendOperation +public enum PngBlendMethod { /// /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 23942dd98..deb01289e 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -152,7 +152,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.currentStream = stream; this.currentStream.Skip(8); Image? image = null; - FrameControl? lastFrameControl = null; + FrameControl? previousFrameControl = null; ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; @@ -182,14 +182,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.FrameControl: - ++frameCount; + frameCount++; if (frameCount == this.maxFrames) { break; } currentFrame = null; - lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + previousFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; case PngChunkType.FrameData: if (frameCount == this.maxFrames) @@ -202,33 +202,32 @@ internal sealed class PngDecoderCore : IImageDecoderInternals PngThrowHelper.ThrowMissingDefaultData(); } - if (lastFrameControl is null) + if (previousFrameControl is null) { PngThrowHelper.ThrowMissingFrameControl(); } if (currentFrame is null) { - this.InitializeFrame(lastFrameControl.Value, image, out currentFrame); + this.InitializeFrame(previousFrameControl.Value, image, out currentFrame); } this.currentStream.Position += 4; - this.ReadScanlines(chunk.Length - 4, currentFrame, pngMetadata, this.ReadNextDataChunkAndSkipSeq, lastFrameControl.Value, cancellationToken); - lastFrameControl = null; + this.ReadScanlines(chunk.Length - 4, currentFrame, pngMetadata, this.ReadNextDataChunkAndSkipSeq, previousFrameControl.Value, cancellationToken); + previousFrameControl = null; break; case PngChunkType.Data: if (image is null) { - this.InitializeImage(metadata, lastFrameControl, out image); + this.InitializeImage(metadata, previousFrameControl, out image); // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } - FrameControl frameControl = lastFrameControl ?? new(0, this.header.Width, this.header.Height, 0, 0, 0, 0, default, default); - - this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, frameControl, cancellationToken); - lastFrameControl = null; + FrameControl frameControl = previousFrameControl ?? new(0, (uint)this.header.Width, (uint)this.header.Height, 0, 0, 0, 0, default, default); + this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, in frameControl, cancellationToken); + previousFrameControl = null; break; case PngChunkType.Palette: this.palette = chunk.Data.GetSpan().ToArray(); @@ -705,9 +704,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals private void DecodePixelData(FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - int currentRow = frameControl.YOffset; + int currentRow = (int)frameControl.YOffset; int currentRowBytesRead = 0; - int height = frameControl.Height; + int height = (int)frameControl.YMax; while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); @@ -771,11 +770,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals private void DecodeInterlacedPixelData(in FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - int currentRow = Adam7.FirstRow[0] + frameControl.YOffset; + int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset; int currentRowBytesRead = 0; int pass = 0; - int width = frameControl.Width; - int height = frameControl.Height; + int width = (int)frameControl.Width; + int endRow = (int)frameControl.YMax; Buffer2D imageBuffer = image.PixelBuffer; while (true) @@ -792,7 +791,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; - while (currentRow < height) + while (currentRow < endRow) { cancellationToken.ThrowIfCancellationRequested(); while (currentRowBytesRead < bytesPerInterlaceScanline) @@ -894,7 +893,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Grayscale: PngScanlineProcessor.ProcessGrayscaleScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, pngMetadata.TransparentColor); @@ -904,7 +903,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)this.bytesPerPixel, @@ -914,7 +913,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Palette: PngScanlineProcessor.ProcessPaletteScanline( - frameControl, + in frameControl, scanlineSpan, rowSpan, pngMetadata.ColorTable); @@ -923,6 +922,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Rgb: PngScanlineProcessor.ProcessRgbScanline( + this.configuration, this.header.BitDepth, frameControl, scanlineSpan, @@ -935,8 +935,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( + this.configuration, this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, this.bytesPerPixel, @@ -984,7 +985,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Grayscale: PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -996,7 +997,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1008,7 +1009,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Palette: PngScanlineProcessor.ProcessInterlacedPaletteScanline( - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1019,8 +1020,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Rgb: PngScanlineProcessor.ProcessInterlacedRgbScanline( + this.configuration, this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -1033,8 +1035,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessInterlacedRgbaScanline( + this.configuration, this.header.BitDepth, - frameControl, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, diff --git a/src/ImageSharp/Formats/Png/PngDisposeOperation.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs similarity index 92% rename from src/ImageSharp/Formats/Png/PngDisposeOperation.cs rename to src/ImageSharp/Formats/Png/PngDisposalMethod.cs index 17a509125..17391de95 100644 --- a/src/ImageSharp/Formats/Png/PngDisposeOperation.cs +++ b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats.Png; @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame). /// -public enum PngDisposeOperation +public enum PngDisposalMethod { /// /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 0eabeeb85..6c86d1b10 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -111,6 +111,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// private const string ColorProfileName = "ICC Profile"; + /// + /// The encoder quantizer, if present. + /// + private IQuantizer? quantizer; + /// /// Initializes a new instance of the class. /// @@ -121,6 +126,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; this.encoder = encoder; + this.quantizer = encoder.Quantizer; } /// @@ -140,63 +146,81 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.height = image.Height; ImageMetadata metadata = image.Metadata; - PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); - Image? clonedImage = null; - Image targetImage = image; + + stream.Write(PngConstants.HeaderBytes); + this.WriteHeaderChunk(stream); + this.WriteGammaChunk(stream); + this.WriteColorProfileChunk(stream, metadata); + + ImageFrame? clonedFrame = null; + ImageFrame currentFrame = image.Frames.RootFrame; + bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; if (clearTransparency) { - targetImage = clonedImage = image.Clone(); - ClearTransparentPixels(targetImage); + currentFrame = clonedFrame = currentFrame.Clone(); + ClearTransparentPixels(currentFrame); } - IndexedImageFrame? rootQuantized = this.CreateQuantizedImageAndUpdateBitDepth(targetImage.Frames.RootFrame); + IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null); + this.WritePaletteChunk(stream, quantized); - stream.Write(PngConstants.HeaderBytes); - - this.WriteHeaderChunk(stream); - this.WriteGammaChunk(stream); - this.WriteColorProfileChunk(stream, metadata); - this.WritePaletteChunk(stream, rootQuantized); this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); this.WriteExifChunk(stream, metadata); this.WriteXmpChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); - if (targetImage.Frames.Count > 1) + if (image.Frames.Count > 1) { - this.WriteAnimationControlChunk(stream, targetImage.Frames.Count, pngMetadata.NumberPlays); + this.WriteAnimationControlChunk(stream, image.Frames.Count, pngMetadata.NumberPlays); + + // TODO: We should attempt to optimize the output by clipping the indexed result to + // non-transparent bounds. That way we can assign frame control bounds and encode + // less data. See GifEncoder for the implementation there. - FrameControl frameControl = this.WriteFrameControlChunk(stream, targetImage.Frames.RootFrame.Metadata.GetPngFrameMetadata(), 0); - _ = this.WriteDataChunks(frameControl, targetImage.Frames.RootFrame, rootQuantized, stream, false); + // Write the first frame. + FrameControl frameControl = this.WriteFrameControlChunk(stream, currentFrame, 0); + this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); - int index = 1; + // Capture the global palette for reuse on subsequent frames. + ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray(); - foreach (ImageFrame imageFrame in ((IEnumerable>)targetImage.Frames).Skip(1)) + // Write following frames. + for (int i = 1; i < image.Frames.Count; i++) { - frameControl = this.WriteFrameControlChunk(stream, imageFrame.Metadata.GetPngFrameMetadata(), index); - index++; - IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(imageFrame); - index += this.WriteDataChunks(frameControl, imageFrame, quantized, stream, true); + currentFrame = image.Frames[i]; + if (clearTransparency) + { + // Dispose of previous clone and reassign. + clonedFrame?.Dispose(); + currentFrame = clonedFrame = currentFrame.Clone(); + ClearTransparentPixels(currentFrame); + } + + frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i); + + // Dispose of previous quantized frame and reassign. quantized?.Dispose(); + quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, previousPalette); + this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true); } } else { - FrameControl frameControl = new(0, this.width, this.height, 0, 0, 0, 0, default, default); - _ = this.WriteDataChunks(frameControl, targetImage.Frames.RootFrame, rootQuantized, stream, false); - rootQuantized?.Dispose(); + FrameControl frameControl = new(0, (uint)this.width, (uint)this.height, 0, 0, 0, 0, default, default); + this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); } this.WriteEndChunk(stream); stream.Flush(); - clonedImage?.Dispose(); - rootQuantized?.Dispose(); + // Dispose of allocations from final frame. + clonedFrame?.Dispose(); + quantized?.Dispose(); } /// @@ -210,46 +234,44 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases. /// /// The type of the pixel. - /// The cloned image where the transparent pixels will be changed. - private static void ClearTransparentPixels(Image image) + /// The cloned image frame where the transparent pixels will be changed. + private static void ClearTransparentPixels(ImageFrame clone) where TPixel : unmanaged, IPixel - { - foreach (ImageFrame imageFrame in image.Frames) + => clone.ProcessPixelRows(accessor => { - imageFrame.ProcessPixelRows(accessor => + // TODO: We should be able to speed this up with SIMD and masking. + Rgba32 rgba32 = default; + Rgba32 transparent = Color.Transparent; + for (int y = 0; y < accessor.Height; y++) { - // TODO: We should be able to speed this up with SIMD and masking. - Rgba32 rgba32 = default; - Rgba32 transparent = Color.Transparent; - for (int y = 0; y < accessor.Height; y++) + Span span = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; x++) { - Span span = accessor.GetRowSpan(y); - for (int x = 0; x < accessor.Width; x++) - { - span[x].ToRgba32(ref rgba32); + span[x].ToRgba32(ref rgba32); - if (rgba32.A is 0) - { - span[x].FromRgba32(transparent); - } + if (rgba32.A is 0) + { + span[x].FromRgba32(transparent); } } - }); - } - } + } + }); /// /// Creates the quantized image and calculates and sets the bit depth. /// /// The type of the pixel. + /// The image metadata. /// The frame to quantize. + /// Any previously derived palette. /// The quantized image. private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth( - ImageFrame frame) + PngMetadata metadata, + ImageFrame frame, + ReadOnlyMemory? previousPalette) where TPixel : unmanaged, IPixel { - IndexedImageFrame? quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, frame); - + IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette); this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); return quantized; } @@ -914,7 +936,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } Span alpha = this.chunkDataBuffer.Span; - switch (pngMetadata.ColorType) + if (pngMetadata.ColorType == PngColorType.Rgb) { if (this.use16Bit) { @@ -957,11 +979,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Writes the animation control chunk to the stream. /// /// The containing image data. - /// Provides APng specific metadata information for the image frame. - /// Sequence number. - private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, int sequenceNumber) + /// The image frame. + /// The frame sequence number. + private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber) { - FrameControl fcTL = FrameControl.FromMetadata(frameMetadata, sequenceNumber); + PngFrameMetadata frameMetadata = imageFrame.Metadata.GetPngFrameMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. + FrameControl fcTL = new( + sequenceNumber: sequenceNumber, + width: (uint)imageFrame.Width, + height: (uint)imageFrame.Height, + xOffset: 0, + yOffset: 0, + delayNumerator: frameMetadata.DelayNumerator, + delayDenominator: frameMetadata.DelayDenominator, + disposeOperation: frameMetadata.DisposalMethod, + blendOperation: frameMetadata.BlendMethod); fcTL.WriteTo(this.chunkDataBuffer.Span); @@ -1036,11 +1070,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable if (isFrame) { - byte[] chunkBuffer = new byte[MaxBlockSize]; - BinaryPrimitives.WriteInt32BigEndian(chunkBuffer, frameControl.SequenceNumber + 1 + i); - buffer.AsSpan().Slice(i * maxBlockSize, length).CopyTo(chunkBuffer.AsSpan(4, length)); - - this.WriteChunk(stream, PngChunkType.FrameData, chunkBuffer, 0, length + 4); + uint sequenceNumber = (uint)(frameControl.SequenceNumber + i); + this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length); } else { @@ -1075,8 +1106,8 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private void EncodePixels(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = frameControl.Width; - int height = frameControl.Height; + int width = (int)frameControl.Width; + int height = (int)frameControl.Height; int bytesPerScanline = this.CalculateScanlineLength(width); int filterLength = bytesPerScanline + 1; @@ -1089,7 +1120,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int y = frameControl.YOffset; y < frameControl.YLimit; y++) + for (int y = (int)frameControl.YOffset; y < frameControl.YMax; y++) { this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y); deflateStream.Write(filter); @@ -1108,13 +1139,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame frame, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = frameControl.Width; - int height = frameControl.Height; + int width = (int)frameControl.XMax; + int height = (int)frameControl.YMax; Buffer2D pixelBuffer = frame.PixelBuffer; for (int pass = 0; pass < 7; pass++) { - int startRow = Adam7.FirstRow[pass] + frameControl.YOffset; - int startCol = Adam7.FirstColumn[pass] + frameControl.XOffset; + int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset; + int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset; int blockWidth = Adam7.ComputeBlockWidth(width, pass); int bytesPerScanline = this.bytesPerPixel <= 1 @@ -1132,11 +1163,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int row = startRow; row < frameControl.YLimit; row += Adam7.RowIncrement[pass]) + for (int row = startRow; row < height; row += Adam7.RowIncrement[pass]) { // Collect pixel data Span srcRow = pixelBuffer.DangerousGetRowSpan(row); - for (int col = startCol, i = 0; col < frameControl.XLimit; col += Adam7.ColumnIncrement[pass]) + for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass]) { block[i++] = srcRow[col]; } @@ -1162,12 +1193,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private void EncodeAdam7IndexedPixels(FrameControl frameControl, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = frameControl.Width; - int height = frameControl.Height; + int width = (int)frameControl.Width; + int endRow = (int)frameControl.YMax; for (int pass = 0; pass < 7; pass++) { - int startRow = Adam7.FirstRow[pass] + frameControl.YOffset; - int startCol = Adam7.FirstColumn[pass] + frameControl.XOffset; + int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset; + int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset; int blockWidth = Adam7.ComputeBlockWidth(width, pass); int bytesPerScanline = this.bytesPerPixel <= 1 @@ -1186,14 +1217,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int row = startRow; - row < frameControl.YLimit; - row += Adam7.RowIncrement[pass]) + for (int row = startRow; row < endRow; row += Adam7.RowIncrement[pass]) { // Collect data ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row); for (int col = startCol, i = 0; - col < frameControl.XLimit; + col < frameControl.XMax; col += Adam7.ColumnIncrement[pass]) { block[i] = srcRow[col]; @@ -1229,7 +1258,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The to write to. /// The type of chunk to write. - /// The containing data. + /// 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) @@ -1255,6 +1284,38 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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); + + uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer + + if (data.Length > 0 && length > 0) + { + stream.Write(data, offset, length); + + crc = Crc32.Calculate(crc, data.Slice(offset, length)); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, crc); + + stream.Write(buffer, 0, 4); // write the crc + } + /// /// Calculates the scanline length. /// @@ -1335,12 +1396,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The png encoder. /// The color type. /// The bits per component. - /// The frame. - private static IndexedImageFrame? CreateQuantizedFrame( + /// The image metadata. + /// The frame to quantize. + /// Any previously derived palette. + private IndexedImageFrame? CreateQuantizedFrame( QuantizingImageEncoder encoder, PngColorType colorType, byte bitDepth, - ImageFrame frame) + PngMetadata metadata, + ImageFrame frame, + ReadOnlyMemory? previousPalette) where TPixel : unmanaged, IPixel { if (colorType is not PngColorType.Palette) @@ -1348,25 +1413,30 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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, -1); + paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); + return paletteQuantizer.QuantizeFrame(frame, frame.Bounds()); + } + // Use the metadata to determine what quantization depth to use if no quantizer has been set. - IQuantizer quantizer = encoder.Quantizer; - if (quantizer is null) + if (this.quantizer is null) { - // TODO: Can APNG have per-frame color tables? - PngMetadata metadata = image.Metadata.GetPngMetadata(); if (metadata.ColorTable is not null) { - // Use the provided palette in total. The caller is responsible for setting values. - quantizer = new PaletteQuantizer(metadata.ColorTable.Value); + // Use the provided palette. The caller is responsible for setting values. + this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value); } else { - quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); + this.quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); } } // Create quantized frame returning the palette and set the bit depth. - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(frame.Configuration); + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration); frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index 76d433056..a68d45ae0 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -23,55 +23,31 @@ public class PngFrameMetadata : IDeepCloneable /// The metadata to create an instance from. private PngFrameMetadata(PngFrameMetadata other) { - this.Width = other.Width; - this.Height = other.Height; - this.XOffset = other.XOffset; - this.YOffset = other.YOffset; - this.DelayNumber = other.DelayNumber; + this.DelayNumerator = other.DelayNumerator; this.DelayDenominator = other.DelayDenominator; - this.DisposeOperation = other.DisposeOperation; - this.BlendOperation = other.BlendOperation; + this.DisposalMethod = other.DisposalMethod; + this.BlendMethod = other.BlendMethod; } - /// - /// Gets or sets the width of the following frame - /// - public int Width { get; set; } - - /// - /// Gets or sets the height of the following frame - /// - public int Height { get; set; } - - /// - /// Gets or sets the X position at which to render the following frame - /// - public int XOffset { get; set; } - - /// - /// Gets or sets the Y position at which to render the following frame - /// - public int YOffset { get; set; } - /// /// Gets or sets the frame delay fraction numerator /// - public short DelayNumber { get; set; } + public ushort DelayNumerator { get; set; } /// /// Gets or sets the frame delay fraction denominator /// - public short DelayDenominator { get; set; } + public ushort DelayDenominator { get; set; } /// /// Gets or sets the type of frame area disposal to be done after rendering this frame /// - public PngDisposeOperation DisposeOperation { get; set; } + public PngDisposalMethod DisposalMethod { get; set; } /// /// Gets or sets the type of frame area rendering for this frame /// - public PngBlendOperation BlendOperation { get; set; } + public PngBlendMethod BlendMethod { get; set; } /// /// Initializes a new instance of the class. @@ -79,14 +55,10 @@ public class PngFrameMetadata : IDeepCloneable /// The chunk to create an instance from. internal void FromChunk(FrameControl frameControl) { - this.Width = frameControl.Width; - this.Height = frameControl.Height; - this.XOffset = frameControl.XOffset; - this.YOffset = frameControl.YOffset; - this.DelayNumber = frameControl.DelayNumber; + this.DelayNumerator = frameControl.DelayNumerator; this.DelayDenominator = frameControl.DelayDenominator; - this.DisposeOperation = frameControl.DisposeOperation; - this.BlendOperation = frameControl.BlendOperation; + this.DisposalMethod = frameControl.DisposeOperation; + this.BlendMethod = frameControl.BlendOperation; } /// diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 9d219e1de..31a59188e 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -20,9 +20,7 @@ internal static class PngScanlineProcessor in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) + Color? transparentColor) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleScanline( bitDepth, @@ -31,9 +29,7 @@ internal static class PngScanlineProcessor rowSpan, 0, 1, - hasTrans, - luminance16Trans, - luminanceTrans); + transparentColor); public static void ProcessInterlacedGrayscaleScanline( int bitDepth, @@ -56,7 +52,7 @@ internal static class PngScanlineProcessor if (bitDepth == 16) { int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += 2) + for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2) { ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); pixel.FromL16(Unsafe.As(ref luminance)); @@ -65,7 +61,7 @@ internal static class PngScanlineProcessor } else { - for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) + for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++) { byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); pixel.FromL8(Unsafe.As(ref luminance)); @@ -81,7 +77,7 @@ internal static class PngScanlineProcessor L16 transparent = transparentColor.Value.ToPixel(); La32 source = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += 2) + for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2) { ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); source.L = luminance; @@ -95,8 +91,7 @@ internal static class PngScanlineProcessor { byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); La16 source = default; - byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); - for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) + for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++) { byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); source.L = luminance; @@ -146,7 +141,7 @@ internal static class PngScanlineProcessor { La32 source = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += 4) + for (nuint x = offset; x < frameControl.XMax; x += increment, o += 4) { source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); @@ -159,7 +154,7 @@ internal static class PngScanlineProcessor { La16 source = default; nuint offset2 = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment) + for (nuint x = offset; x < frameControl.XMax; x += increment) { source.L = Unsafe.Add(ref scanlineSpanRef, offset2); source.A = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample); @@ -175,8 +170,7 @@ internal static class PngScanlineProcessor in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel => ProcessInterlacedPaletteScanline( frameControl, @@ -184,8 +178,7 @@ internal static class PngScanlineProcessor rowSpan, 0, 1, - palette, - paletteAlpha); + palette); public static void ProcessInterlacedPaletteScanline( in FrameControl frameControl, @@ -193,8 +186,7 @@ internal static class PngScanlineProcessor Span rowSpan, uint pixelOffset, uint increment, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { if (palette is null) @@ -202,53 +194,31 @@ internal static class PngScanlineProcessor PngThrowHelper.ThrowMissingPalette(); } - uint offset = pixelOffset + (uint)frameControl.XOffset; TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span); - for (nuint x = 0; x < (uint)header.Width; x++) + for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++) { - // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha - // channel and we should try to read it. - Rgba32 rgba = default; - ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha); - for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) - { - uint index = Unsafe.Add(ref scanlineSpanRef, o); - rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; - rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) - { - int index = Unsafe.Add(ref scanlineSpanRef, o); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + uint index = Unsafe.Add(ref scanlineSpanRef, o); + pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32()); + Unsafe.Add(ref rowSpanRef, x) = pixel; } } public static void ProcessRgbScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) + Color? transparentColor) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbScanline( + configuration, bitDepth, frameControl, scanlineSpan, @@ -257,11 +227,10 @@ internal static class PngScanlineProcessor 1, bytesPerPixel, bytesPerSample, - hasTrans, - rgb48Trans, - rgb24Trans); + transparentColor); public static void ProcessInterlacedRgbScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -274,36 +243,18 @@ internal static class PngScanlineProcessor where TPixel : unmanaged, IPixel { uint offset = pixelOffset + (uint)frameControl.XOffset; + TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - bool hasTransparency = transparentColor is not null; - if (bitDepth == 16) + if (transparentColor is null) { - if (hasTrans) - { - Rgb48 rgb48 = default; - Rgba64 rgba64 = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) - { - rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); - rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); - rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); - - rgba64.Rgb = rgb48; - rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.FromRgba64(rgba64); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else + if (bitDepth == 16) { Rgb48 rgb48 = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) { rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -315,30 +266,33 @@ internal static class PngScanlineProcessor } else { - Rgb24 rgb = default; - int o = 0; - for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) - { - rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + // Rgb24 rgb = default; + // int o = 0; + // for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) + // { + // rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); + // rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + // rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + + // pixel.FromRgb24(rgb); + // Unsafe.Add(ref rowSpanRef, x) = pixel; + // } + + // PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width); + PixelOperations.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan[(int)offset..], (int)frameControl.XMax); } return; } - if (header.BitDepth == 16) + if (bitDepth == 16) { Rgb48 transparent = transparentColor.Value.ToPixel(); Rgb48 rgb48 = default; Rgba64 rgba64 = default; int o = 0; - for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) { rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -357,7 +311,7 @@ internal static class PngScanlineProcessor Rgba32 rgba = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) { rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); @@ -368,23 +322,10 @@ internal static class PngScanlineProcessor Unsafe.Add(ref rowSpanRef, x) = pixel; } } - else - { - Rgb24 rgb = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) - { - rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } } public static void ProcessRgbaScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -393,6 +334,7 @@ internal static class PngScanlineProcessor int bytesPerSample) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbaScanline( + configuration, bitDepth, frameControl, scanlineSpan, @@ -403,6 +345,7 @@ internal static class PngScanlineProcessor bytesPerSample); public static void ProcessInterlacedRgbaScanline( + Configuration configuration, int bitDepth, in FrameControl frameControl, ReadOnlySpan scanlineSpan, @@ -421,7 +364,7 @@ internal static class PngScanlineProcessor { Rgba64 rgba64 = default; int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) { rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -434,19 +377,22 @@ internal static class PngScanlineProcessor } else { - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - Rgba32 rgba = default; - int o = 0; - for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) - { - rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); - rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - rgba.A = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + // ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + // Rgba32 rgba = default; + // int o = 0; + // for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) + // { + // rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); + // rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + // rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + // rgba.A = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + + // pixel.FromRgba32(rgba); + // Unsafe.Add(ref rowSpanRef, x) = pixel; + // } + + // PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width); + PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan[(int)offset..], (int)frameControl.XMax); } } }