From d1929412289d3c8bb5bc3974cc47e0b811f0e75f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 1 Feb 2022 10:23:08 +0100 Subject: [PATCH] Add lossless alpha compression --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 86 ++++++++++++++++++- .../Formats/Webp/BitWriter/BitWriterBase.cs | 11 ++- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 10 ++- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 31 ++++++- .../Formats/Webp/Lossy/Vp8Encoder.cs | 14 +-- .../Formats/Webp/WebpEncoderCore.cs | 6 +- 6 files changed, 134 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 06c114c71f..571da5bb24 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -3,18 +3,98 @@ using System; using System.Buffers; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp { /// - /// Encodes the alpha channel data. - /// Data is either compressed as lossless webp image or uncompressed. + /// Methods for encoding the alpha data of a VP8 image. /// internal static class AlphaEncoder { - public static byte[] EncodeAlpha(Image image, Configuration configuration, MemoryAllocator memoryAllocator) + /// + /// Encodes the alpha channel data. + /// Data is either compressed as lossless webp image or uncompressed. + /// + /// The pixel format. + /// The to encode from. + /// The global configuration. + /// The memory manager. + /// Indicates, if the data should be compressed with the lossless webp compression. + /// The alpha data. + public static byte[] EncodeAlpha(Image image, Configuration configuration, MemoryAllocator memoryAllocator, bool compress) + where TPixel : unmanaged, IPixel + { + byte[] alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator); + int width = image.Width; + int height = image.Height; + if (compress) + { + WebpEncodingMethod effort = WebpEncodingMethod.Default; + int quality = 8 * (int)effort; + using var lossLessEncoder = new Vp8LEncoder( + memoryAllocator, + configuration, + width, + height, + quality, + effort, + WebpTransparentColorMode.Preserve, + false, + 0); + + // The transparency information will be stored in the green channel of the ARGB quadruplet. + // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, + // that can improve compression. + using Image alphaAsImage = DispatchAlphaToGreen(image, alphaData); + + return lossLessEncoder.EncodeAlphaImageData(alphaAsImage); + } + + return alphaData; + } + + /// + /// Store the transparency in the green channel. + /// + /// The pixel format. + /// The to encode from. + /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. + /// The transparency image. + private static Image DispatchAlphaToGreen(Image image, byte[] alphaData) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + var alphaAsImage = new Image(width, height); + + for (int y = 0; y < height; y++) + { + Memory rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y); + Span pixelRow = rowBuffer.Span; + Span alphaRow = alphaData.AsSpan(y * width, width); + for (int x = 0; x < width; x++) + { + // Leave A/R/B channels zero'd. + pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0); + } + } + + return alphaAsImage; + } + + /// + /// Extract the alpha data of the image. + /// + /// The pixel format. + /// The to encode from. + /// The global configuration. + /// The memory manager. + /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. + private static byte[] ExtractAlphaChannel(Image image, Configuration configuration, MemoryAllocator memoryAllocator) where TPixel : unmanaged, IPixel { Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index b33f7987c1..84c9d3f133 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -146,7 +146,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// /// The stream to write to. /// The alpha channel data bytes. - protected void WriteAlphaChunk(Stream stream, byte[] dataBytes) + /// Indicates, if the alpha channel data is compressed. + protected void WriteAlphaChunk(Stream stream, byte[] dataBytes, bool alphaDataIsCompressed) { DebugGuard.NotNull(dataBytes, nameof(dataBytes)); @@ -157,9 +158,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter BinaryPrimitives.WriteUInt32LittleEndian(buf, size); stream.Write(buf); - // Write flags, all zero for now. - stream.WriteByte(0); + byte flags = 0; + if (alphaDataIsCompressed) + { + flags |= 1; + } + stream.WriteByte(flags); stream.Write(dataBytes); // Add padding byte if needed. diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 3f16fc89bc..577a87e6a1 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -410,7 +410,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// The height of the image. /// Flag indicating, if a alpha channel is present. /// The alpha channel data. - public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha, byte[] alphaData) + /// Indicates, if the alpha data is compressed. + public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha, byte[] alphaData, bool alphaDataIsCompressed) { bool isVp8X = false; byte[] exifBytes = null; @@ -461,7 +462,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha, alphaData); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -660,7 +661,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter ExifProfile exifProfile, XmpProfile xmpProfile, bool hasAlpha, - byte[] alphaData) + byte[] alphaData, + bool alphaDataIsCompressed) { this.WriteRiffHeader(stream, riffSize); @@ -670,7 +672,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); if (hasAlpha) { - this.WriteAlphaChunk(stream, alphaData); + this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index e9dce913a3..797d0794f9 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -228,7 +228,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless public Vp8LHashChain HashChain { get; } /// - /// Encodes the image to the specified stream from the . + /// Encodes the image as lossless webp to the specified stream. /// /// The pixel format. /// The to encode from. @@ -236,10 +236,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel { - image.Metadata.SyncProfiles(); int width = image.Width; int height = image.Height; + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + // Convert image pixels to bgra array. bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); @@ -253,11 +255,32 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - ImageMetadata metadata = image.Metadata; - metadata.SyncProfiles(); this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha); } + /// + /// Encodes the alpha image data using the webp lossless compression. + /// + /// The type of the pixel. + /// The to encode from. + /// The encoded alpha stream. + public byte[] EncodeAlphaImageData(Image image) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + + // Convert image pixels to bgra array. + this.ConvertPixelsToBgra(image, width, height); + + // The image-stream does NOT contain any headers describing the image dimension, the dimension is already known. + this.EncodeStream(image); + this.bitWriter.Finish(); + using var ms = new MemoryStream(); + this.bitWriter.WriteToStream(ms); + return ms.ToArray(); + } + /// /// Writes the image size to the bitwriter buffer. /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 48af53960c..d8bd8f759c 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -71,10 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// private int uvAlpha; - /// - /// Scratch buffer to reduce allocations. - /// - private readonly int[] scratch = new int[16]; + private readonly bool alphaCompression; private readonly byte[] averageBytesPerMb = { 50, 24, 16, 9, 7, 5, 3, 2 }; @@ -105,6 +102,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// Number of entropy-analysis passes (in [1..10]). /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). /// The spatial noise shaping. 0=off, 100=maximum. + /// If true, the alpha channel will be compressed with the lossless compression. public Vp8Encoder( MemoryAllocator memoryAllocator, Configuration configuration, @@ -114,7 +112,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy WebpEncodingMethod method, int entropyPasses, int filterStrength, - int spatialNoiseShaping) + int spatialNoiseShaping, + bool alphaCompression) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; @@ -125,6 +124,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10); this.filterStrength = Numerics.Clamp(filterStrength, 0, 100); this.spatialNoiseShaping = Numerics.Clamp(spatialNoiseShaping, 0, 100); + this.alphaCompression = alphaCompression; this.rdOptLevel = method is WebpEncodingMethod.BestQuality ? Vp8RdLevel.RdOptTrellisAll : method >= WebpEncodingMethod.Level5 ? Vp8RdLevel.RdOptTrellis : method >= WebpEncodingMethod.Level3 ? Vp8RdLevel.RdOptBasic @@ -327,7 +327,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy if (hasAlpha) { // TODO: This can potentially run in an separate task. - alphaData = AlphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator); + alphaData = AlphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.alphaCompression); } // Stats-collection loop. @@ -363,7 +363,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy // Write bytes from the bitwriter buffer to the stream. ImageMetadata metadata = image.Metadata; metadata.SyncProfiles(); - this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha, alphaData); + this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha, alphaData, this.alphaCompression); } /// diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 195fa62bdc..deed08b729 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -22,7 +22,6 @@ namespace SixLabors.ImageSharp.Formats.Webp private readonly MemoryAllocator memoryAllocator; /// - /// TODO: not used at the moment. /// Indicating whether the alpha plane should be compressed with Webp lossless format. /// private readonly bool alphaCompression; @@ -100,7 +99,7 @@ namespace SixLabors.ImageSharp.Formats.Webp } /// - /// Encodes the image to the specified stream from the . + /// Encodes the image as webp to the specified stream. /// /// The pixel format. /// The to encode from. @@ -149,7 +148,8 @@ namespace SixLabors.ImageSharp.Formats.Webp this.method, this.entropyPasses, this.filterStrength, - this.spatialNoiseShaping); + this.spatialNoiseShaping, + this.alphaCompression); enc.Encode(image, stream); } }