From bc5b6c519b92badc7079efcfcffa545275a94682 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 Oct 2023 22:29:58 +1000 Subject: [PATCH] Add alpha blending support --- .../Formats/Png/Chunks/FrameControl.cs | 5 ++ src/ImageSharp/Formats/Png/PngBlendMethod.cs | 8 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 73 ++++++++++++++++--- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- 4 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs index c7233ada1..b912e9b09 100644 --- a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs +++ b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs @@ -9,6 +9,11 @@ internal readonly struct FrameControl { public const int Size = 26; + public FrameControl(uint width, uint height) + : this(0, width, height, 0, 0, 0, 0, default, default) + { + } + public FrameControl( uint sequenceNumber, uint width, diff --git a/src/ImageSharp/Formats/Png/PngBlendMethod.cs b/src/ImageSharp/Formats/Png/PngBlendMethod.cs index b7ace9ccf..f71dce832 100644 --- a/src/ImageSharp/Formats/Png/PngBlendMethod.cs +++ b/src/ImageSharp/Formats/Png/PngBlendMethod.cs @@ -1,10 +1,11 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. 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. +/// 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 PngBlendMethod { @@ -14,7 +15,8 @@ public enum PngBlendMethod Source, /// - /// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. Note that the second variation of the sample code is applicable. + /// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as + /// described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. /// Over } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 8484fd0c6..776e52a33 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -153,6 +153,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.currentStream.Skip(8); Image? image = null; FrameControl? previousFrameControl = null; + ImageFrame? previousFrame = null; ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; @@ -213,7 +214,21 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } this.currentStream.Position += 4; - this.ReadScanlines(chunk.Length - 4, currentFrame, pngMetadata, this.ReadNextDataChunkAndSkipSeq, previousFrameControl.Value, cancellationToken); + this.ReadScanlines( + chunk.Length - 4, + currentFrame, + pngMetadata, + this.ReadNextDataChunkAndSkipSeq, + previousFrameControl.Value, + cancellationToken); + + PngFrameMetadata pngFrameMetadata = currentFrame.Metadata.GetPngFrameMetadata(); + if (previousFrame != null && pngFrameMetadata.BlendMethod == PngBlendMethod.Over) + { + this.AlphaBlend(previousFrame, currentFrame); + } + + previousFrame = currentFrame; previousFrameControl = null; break; case PngChunkType.Data: @@ -225,8 +240,15 @@ internal sealed class PngDecoderCore : IImageDecoderInternals AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } - 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); + FrameControl frameControl = previousFrameControl ?? new((uint)this.header.Width, (uint)this.header.Height); + this.ReadScanlines( + chunk.Length, + image.Frames.RootFrame, + pngMetadata, + this.ReadNextDataChunk, + in frameControl, + cancellationToken); + previousFrameControl = null; break; case PngChunkType.Palette: @@ -698,10 +720,15 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The pixel format. /// The frame control /// The compressed pixel data stream. - /// The image to decode to. + /// The image frame to decode to. /// The png metadata /// The CancellationToken - private void DecodePixelData(FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void DecodePixelData( + FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int currentRow = (int)frameControl.YOffset; @@ -750,8 +777,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } - this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, image, pngMetadata); - + this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata); this.SwapScanlineBuffers(); currentRow++; } @@ -759,7 +785,6 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// Decodes the raw interlaced pixel data row by row - /// /// /// The pixel format. /// The frame control @@ -767,7 +792,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The current image. /// The png metadata. /// The cancellation token. - private void DecodeInterlacedPixelData(in FrameControl frameControl, DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void DecodeInterlacedPixelData( + in FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame image, + PngMetadata pngMetadata, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset; @@ -845,6 +875,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); + // TODO: Alpha blending. this.SwapScanlineBuffers(); currentRow += Adam7.RowIncrement[pass]; @@ -874,7 +905,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The de-filtered scanline /// The image /// The png metadata. - private void ProcessDefilteredScanline(in FrameControl frameControl, int currentRow, ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) + private void ProcessDefilteredScanline( + in FrameControl frameControl, + int currentRow, + ReadOnlySpan defilteredScanline, + ImageFrame pixels, + PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); @@ -1841,4 +1877,21 @@ internal sealed class PngDecoderCore : IImageDecoderInternals private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); + + private void AlphaBlend(ImageFrame src, ImageFrame dst) + where TPixel : unmanaged, IPixel + { + Buffer2D srcPixels = src.PixelBuffer; + Buffer2D dstPixels = dst.PixelBuffer; + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + + for (int y = 0; y < src.Height; y++) + { + Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); + Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); + + blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); + } + } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 8eabde8d9..ef179e826 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -214,7 +214,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } else { - FrameControl frameControl = new(0, (uint)this.width, (uint)this.height, 0, 0, 0, 0, default, default); + FrameControl frameControl = new((uint)this.width, (uint)this.height); this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false); }