From 96c1c725f94dcb8d5882c22af68c3f46a9d5582e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 30 Jan 2022 18:41:56 +0100 Subject: [PATCH] Write ALPH chunk (only uncompressed for now) --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 42 ++++++++++++++++++ .../Formats/Webp/BitWriter/BitWriterBase.cs | 43 ++++++++++++++++++- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 35 ++++++++++++--- .../Formats/Webp/Lossy/Vp8Encoder.cs | 13 ++++-- .../Formats/Webp/Lossy/YuvConversion.cs | 11 ++++- 5 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/AlphaEncoder.cs diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs new file mode 100644 index 0000000000..06c114c71f --- /dev/null +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +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. + /// + internal static class AlphaEncoder + { + public static byte[] EncodeAlpha(Image image, Configuration configuration, MemoryAllocator memoryAllocator) + where TPixel : unmanaged, IPixel + { + Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + int height = image.Height; + int width = image.Width; + byte[] alphaData = new byte[width * height]; + + using IMemoryOwner rowBuffer = memoryAllocator.Allocate(width); + Span rgbaRow = rowBuffer.GetSpan(); + + for (int y = 0; y < height; y++) + { + Span rowSpan = imageBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgba32(configuration, rowSpan, rgbaRow); + int offset = y * width; + for (int x = 0; x < width; x++) + { + alphaData[offset + x] = rgbaRow[x].A; + } + } + + return alphaData; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ac039be797..b33f7987c1 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// Calculates the chunk size of EXIF or XMP metadata. /// /// The metadata profile bytes. - /// The exif chunk size in bytes. + /// The metadata chunk size in bytes. protected uint MetadataChunkSize(byte[] metadataBytes) { uint metaSize = (uint)metadataBytes.Length; @@ -103,6 +103,19 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter return metaChunkSize; } + /// + /// Calculates the chunk size of a alpha chunk. + /// + /// The alpha chunk bytes. + /// The alpha data chunk size in bytes. + protected uint AlphaChunkSize(byte[] alphaBytes) + { + uint alphaSize = (uint)alphaBytes.Length + 1; + uint alphaChunkSize = WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); + + return alphaChunkSize; + } + /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// @@ -128,6 +141,34 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter } } + /// + /// Writes the alpha chunk to the stream. + /// + /// The stream to write to. + /// The alpha channel data bytes. + protected void WriteAlphaChunk(Stream stream, byte[] dataBytes) + { + DebugGuard.NotNull(dataBytes, nameof(dataBytes)); + + uint size = (uint)dataBytes.Length + 1; + Span buf = this.scratchBuffer.AsSpan(0, 4); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); + + // Write flags, all zero for now. + stream.WriteByte(0); + + stream.Write(dataBytes); + + // Add padding byte if needed. + if ((size & 1) == 1) + { + stream.WriteByte(0); + } + } + /// /// Writes a VP8X header to the stream. /// diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 4e91bedb0b..3f16fc89bc 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -409,7 +409,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) + /// The alpha channel data. + public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha, byte[] alphaData) { bool isVp8X = false; byte[] exifBytes = null; @@ -418,7 +419,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (exifProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); riffSize += this.MetadataChunkSize(exifBytes); } @@ -426,11 +426,21 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (xmpProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; xmpBytes = xmpProfile.Data; riffSize += this.MetadataChunkSize(xmpBytes); } + if (hasAlpha) + { + isVp8X = true; + riffSize += this.AlphaChunkSize(alphaData); + } + + if (isVp8X) + { + riffSize += ExtendedFileChunkSize; + } + this.Finish(); uint numBytes = (uint)this.NumBytes(); int mbSize = this.enc.Mbw * this.enc.Mbh; @@ -451,7 +461,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); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha, alphaData); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -639,7 +649,18 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter while (it.Next()); } - private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, XmpProfile xmpProfile, bool hasAlpha) + private void WriteWebpHeaders( + Stream stream, + uint size0, + uint vp8Size, + uint riffSize, + bool isVp8X, + uint width, + uint height, + ExifProfile exifProfile, + XmpProfile xmpProfile, + bool hasAlpha, + byte[] alphaData) { this.WriteRiffHeader(stream, riffSize); @@ -647,6 +668,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (isVp8X) { this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); + if (hasAlpha) + { + this.WriteAlphaChunk(stream, alphaData); + } } this.WriteVp8Header(stream, vp8Size); diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 0222320502..48af53960c 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -300,7 +300,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -322,8 +322,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy int expectedSize = this.Mbw * this.Mbh * averageBytesPerMacroBlock; this.bitWriter = new Vp8BitWriter(expectedSize, this); - // TODO: EncodeAlpha(); - bool hasAlpha = false; + // Extract and encode alpha data, if present. + byte[] alphaData = null; + if (hasAlpha) + { + // TODO: This can potentially run in an separate task. + alphaData = AlphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator); + } // Stats-collection loop. this.StatLoop(width, height, yStride, uvStride); @@ -358,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); + this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha, alphaData); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index 7a731f4284..878bebd105 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -318,7 +318,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// Span to store the luma component of the image. /// Span to store the u component of the image. /// Span to store the v component of the image. - public static void ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + /// true, if the image contains alpha data. + public static bool ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) where TPixel : unmanaged, IPixel { Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; @@ -335,6 +336,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy Span bgraRow1 = bgraRow1Buffer.GetSpan(); int uvRowIndex = 0; int rowIndex; + bool hasAlpha = false; for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) { Span rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex); @@ -343,6 +345,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy PixelOperations.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1); bool rowsHaveAlpha = WebpCommonUtils.CheckNonOpaque(bgraRow0) && WebpCommonUtils.CheckNonOpaque(bgraRow1); + if (rowsHaveAlpha) + { + hasAlpha = true; + } // Downsample U/V planes, two rows at a time. if (!rowsHaveAlpha) @@ -375,10 +381,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy else { AccumulateRgba(bgraRow0, bgraRow0, tmpRgbSpan, width); + hasAlpha = true; } ConvertRgbaToUv(tmpRgbSpan, u.Slice(uvRowIndex * uvWidth), v.Slice(uvRowIndex * uvWidth), uvWidth); } + + return hasAlpha; } ///