diff --git a/src/ImageSharp/Formats/WebP/AlphaDecoder.cs b/src/ImageSharp/Formats/WebP/AlphaDecoder.cs index f6ee5ec16..bf382c425 100644 --- a/src/ImageSharp/Formats/WebP/AlphaDecoder.cs +++ b/src/ImageSharp/Formats/WebP/AlphaDecoder.cs @@ -4,19 +4,27 @@ using System; using SixLabors.ImageSharp.Formats.WebP.Filters; -using SixLabors.ImageSharp.Processing; +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.WebP { internal class AlphaDecoder { - public int Width { get; set; } + public int Width { get; } - public int Height { get; set; } + public int Height { get; } - public WebPFilterBase Filter { get; set; } + public WebPFilterBase Filter { get; } - private WebPFilterType FilterType { get; } + public WebPFilterType FilterType { get; } + + public int CropTop { get; } + + public int LastRow { get; set; } + + public Vp8LDecoder Vp8LDec { get; } + + public byte[] Alpha { get; } private int PreProcessing { get; } @@ -24,7 +32,7 @@ namespace SixLabors.ImageSharp.Formats.WebP private byte[] Data { get; } - private Vp8LDecoder Vp8LDec { get; set; } + private WebPLosslessDecoder LosslessDecoder { get; } /// /// Although Alpha Channel requires only 1 byte per pixel, @@ -33,14 +41,15 @@ namespace SixLabors.ImageSharp.Formats.WebP /// public bool Use8BDecode { get; set; } - public AlphaDecoder(int width, int height, byte[] data) + public AlphaDecoder(int width, int height, byte[] data, byte alphaChunkHeader, MemoryAllocator memoryAllocator) { this.Width = width; this.Height = height; this.Data = data; + this.LastRow = 0; // Compression method: Either 0 (no compression) or 1 (Compressed using the WebP lossless format) - int method = data[0] & 0x03; + int method = alphaChunkHeader & 0x03; if (method < 0 || method > 1) { WebPThrowHelper.ThrowImageFormatException($"unexpected alpha compression method {method} found"); @@ -49,7 +58,7 @@ namespace SixLabors.ImageSharp.Formats.WebP this.Compressed = !(method is 0); // The filtering method used. Only values between 0 and 3 are valid. - int filter = (data[0] >> 2) & 0x03; + int filter = (alphaChunkHeader >> 2) & 0x03; if (filter < 0 || filter > 3) { WebPThrowHelper.ThrowImageFormatException($"unexpected alpha filter method {filter} found"); @@ -60,16 +69,49 @@ namespace SixLabors.ImageSharp.Formats.WebP // These INFORMATIVE bits are used to signal the pre-processing that has been performed during compression. // The decoder can use this information to e.g. dither the values or smooth the gradients prior to display. // 0: no pre-processing, 1: level reduction - this.PreProcessing = (data[0] >> 4) & 0x03; + this.PreProcessing = (alphaChunkHeader >> 4) & 0x03; + + this.Vp8LDec = new Vp8LDecoder(width, height, memoryAllocator); + + // TODO: use memory allocator + this.Alpha = new byte[width * height]; + + if (this.Compressed) + { + var bitReader = new Vp8LBitReader(data); + this.LosslessDecoder = new WebPLosslessDecoder(bitReader, memoryAllocator); + this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true); + } } private int PrevLineOffset { get; set; } - public void Decode(Vp8Decoder dec, Span dst) + public void Decode() { if (this.Compressed is false) { - this.Data.AsSpan(1, this.Width * this.Height).CopyTo(dst); + if (this.Data.Length < (this.Width * this.Height)) + { + WebPThrowHelper.ThrowImageFormatException("not enough data in the ALPH chunk"); + } + + switch (this.FilterType) + { + case WebPFilterType.None: + this.Data.AsSpan(0, this.Width * this.Height).CopyTo(this.Alpha); + break; + case WebPFilterType.Horizontal: + + break; + case WebPFilterType.Vertical: + break; + case WebPFilterType.Gradient: + break; + } + } + else + { + this.LosslessDecoder.DecodeAlphaData(this); } } @@ -82,24 +124,26 @@ namespace SixLabors.ImageSharp.Formats.WebP int outputOffset, int stride) { - if (!(this.Filter is WebPFilterNone)) + if (this.Filter is WebPFilterNone) { - int prevLineOffset = this.PrevLineOffset; + return; + } - for (int y = firstRow; y < lastRow; y++) - { - this.Filter - .Unfilter( - prevLine, - prevLineOffset, - output, - outputOffset, - output, - outputOffset, - stride); - prevLineOffset = outputOffset; - outputOffset += stride; - } + int prevLineOffset = this.PrevLineOffset; + + for (int y = firstRow; y < lastRow; y++) + { + this.Filter + .Unfilter( + prevLine, + prevLineOffset, + output, + outputOffset, + output, + outputOffset, + stride); + prevLineOffset = outputOffset; + outputOffset += stride; } } } diff --git a/src/ImageSharp/Formats/WebP/Filters/WebPFilterBase.cs b/src/ImageSharp/Formats/WebP/Filters/WebPFilterBase.cs index 4ff2ae568..74b69f43a 100644 --- a/src/ImageSharp/Formats/WebP/Filters/WebPFilterBase.cs +++ b/src/ImageSharp/Formats/WebP/Filters/WebPFilterBase.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Filters // Fast // } - abstract class WebPFilterBase + internal abstract class WebPFilterBase { /// /// diff --git a/src/ImageSharp/Formats/WebP/Filters/WebPFilterHorizontal.cs b/src/ImageSharp/Formats/WebP/Filters/WebPFilterHorizontal.cs index 4328332a5..fdec37e5d 100644 --- a/src/ImageSharp/Formats/WebP/Filters/WebPFilterHorizontal.cs +++ b/src/ImageSharp/Formats/WebP/Filters/WebPFilterHorizontal.cs @@ -5,7 +5,7 @@ using System; namespace SixLabors.ImageSharp.Formats.WebP.Filters { - class WebPFilterHorizontal : WebPFilterBase + internal class WebPFilterHorizontal : WebPFilterBase { public override void Unfilter( Span prevLine, @@ -63,7 +63,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Filters predsOffset = inputOffset; } - if (row == 0) + if (row is 0) { // leftmost pixel is the same as Input for topmost scanline output[0] = input[0]; diff --git a/src/ImageSharp/Formats/WebP/Vp8Decoder.cs b/src/ImageSharp/Formats/WebP/Vp8Decoder.cs index 7dab51e33..8de3144f6 100644 --- a/src/ImageSharp/Formats/WebP/Vp8Decoder.cs +++ b/src/ImageSharp/Formats/WebP/Vp8Decoder.cs @@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Formats.WebP this.TmpYBuffer = new byte[(width * height) + extraY]; // TODO: figure out min buffer length this.TmpUBuffer = new byte[(width * height) + extraUv]; // TODO: figure out min buffer length this.TmpVBuffer = new byte[(width * height) + extraUv]; // TODO: figure out min buffer length - this.Bgr = new byte[width * height * 4]; + this.Pixels = new byte[width * height * 4]; for (int i = 0; i < this.YuvBuffer.Length; i++) { @@ -202,7 +202,7 @@ namespace SixLabors.ImageSharp.Formats.WebP public byte[] TmpVBuffer { get; } - public byte[] Bgr { get; } + public byte[] Pixels { get; } /// /// Gets or sets filter strength info. diff --git a/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs b/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs index 9dba6c91d..f4114f5be 100644 --- a/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs +++ b/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs @@ -61,7 +61,29 @@ namespace SixLabors.ImageSharp.Formats.WebP /// /// True if a bit was read past the end of buffer. /// - private bool eos; + public bool Eos; + + /// + /// Initializes a new instance of the class. + /// + /// Lossless compressed image data. + public Vp8LBitReader(byte[] data) + { + this.Data = data; + this.len = data.Length; + this.value = 0; + this.bitPos = 0; + this.Eos = false; + + ulong currentValue = 0; + for (int i = 0; i < 8; ++i) + { + currentValue |= (ulong)this.Data[i] << (8 * i); + } + + this.value = currentValue; + this.pos = 8; + } /// /// Initializes a new instance of the class. @@ -78,7 +100,7 @@ namespace SixLabors.ImageSharp.Formats.WebP this.len = length; this.value = 0; this.bitPos = 0; - this.eos = false; + this.Eos = false; if (length > sizeof(long)) { @@ -104,7 +126,7 @@ namespace SixLabors.ImageSharp.Formats.WebP { Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); - if (!this.eos && nBits <= Vp8LMaxNumBitRead) + if (!this.Eos && nBits <= Vp8LMaxNumBitRead) { ulong val = this.PrefetchBits() & this.bitMask[nBits]; int newBits = this.bitPos + nBits; @@ -139,7 +161,7 @@ namespace SixLabors.ImageSharp.Formats.WebP /// /// Return the pre-fetched bits, so they can be looked up. /// - /// Prefetched bits. + /// The pre-fetched bits. public ulong PrefetchBits() { return this.value >> (this.bitPos & (Vp8LLbits - 1)); @@ -162,7 +184,7 @@ namespace SixLabors.ImageSharp.Formats.WebP /// True, if end of buffer was reached. public bool IsEndOfStream() { - return this.eos || ((this.pos == this.len) && (this.bitPos > Vp8LLbits)); + return this.Eos || ((this.pos == this.len) && (this.bitPos > Vp8LLbits)); } private void DoFillBitWindow() @@ -191,7 +213,7 @@ namespace SixLabors.ImageSharp.Formats.WebP private void SetEndOfStream() { - this.eos = true; + this.Eos = true; this.bitPos = 0; // To avoid undefined behaviour with shifts. } } diff --git a/src/ImageSharp/Formats/WebP/Vp8LDecoder.cs b/src/ImageSharp/Formats/WebP/Vp8LDecoder.cs index bf8d949b2..968d98b9d 100644 --- a/src/ImageSharp/Formats/WebP/Vp8LDecoder.cs +++ b/src/ImageSharp/Formats/WebP/Vp8LDecoder.cs @@ -1,25 +1,31 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Buffers; using System.Collections.Generic; +using SixLabors.Memory; + namespace SixLabors.ImageSharp.Formats.WebP { /// /// Holds information for decoding a lossless webp image. /// - internal class Vp8LDecoder + internal class Vp8LDecoder : IDisposable { /// /// Initializes a new instance of the class. /// /// The width of the image. /// The height of the image. - public Vp8LDecoder(int width, int height) + /// Used for allocating memory for the pixel data output. + public Vp8LDecoder(int width, int height, MemoryAllocator memoryAllocator) { this.Width = width; this.Height = height; this.Metadata = new Vp8LMetadata(); + this.Pixels = memoryAllocator.Allocate(width * height, AllocationOptions.Clean); } /// @@ -41,5 +47,22 @@ namespace SixLabors.ImageSharp.Formats.WebP /// Gets or sets the transformations which needs to be reversed. /// public List Transforms { get; set; } + + /// + /// Gets the pixel data. + /// + public IMemoryOwner Pixels { get; } + + /// + public void Dispose() + { + this.Pixels.Dispose(); + foreach (Vp8LTransform transform in this.Transforms) + { + transform.Data?.Dispose(); + } + + this.Metadata?.HuffmanImage?.Dispose(); + } } } diff --git a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs index f55bc0ed0..a9312f19c 100644 --- a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs @@ -242,6 +242,7 @@ namespace SixLabors.ImageSharp.Formats.WebP } byte[] alphaData = null; + byte alphaChunkHeader = 0; if (isAlphaPresent) { chunkType = this.ReadChunkType(); @@ -251,8 +252,9 @@ namespace SixLabors.ImageSharp.Formats.WebP } uint alphaChunkSize = this.ReadChunkSize(); - alphaData = new byte[alphaChunkSize]; - this.currentStream.Read(alphaData, 0, (int)alphaChunkSize); + alphaChunkHeader = (byte)this.currentStream.ReadByte(); + alphaData = new byte[alphaChunkSize - 1]; + this.currentStream.Read(alphaData, 0, alphaData.Length); } var features = new WebPFeatures() @@ -260,6 +262,7 @@ namespace SixLabors.ImageSharp.Formats.WebP Animation = isAnimationPresent, Alpha = isAlphaPresent, AlphaData = alphaData, + AlphaChunkHeader = alphaChunkHeader, ExifProfile = isExifPresent, IccProfile = isIccPresent, XmpMetaData = isXmpPresent diff --git a/src/ImageSharp/Formats/WebP/WebPFeatures.cs b/src/ImageSharp/Formats/WebP/WebPFeatures.cs index f0d728402..3fd035076 100644 --- a/src/ImageSharp/Formats/WebP/WebPFeatures.cs +++ b/src/ImageSharp/Formats/WebP/WebPFeatures.cs @@ -23,6 +23,11 @@ namespace SixLabors.ImageSharp.Formats.WebP /// public byte[] AlphaData { get; set; } + /// + /// Gets or sets the alpha chunk header. + /// + public byte AlphaChunkHeader { get; set; } + /// /// Gets or sets a value indicating whether this image has a EXIF Profile. /// diff --git a/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs b/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs index 98fe46cbc..b64cc0a17 100644 --- a/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs +++ b/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs @@ -5,7 +5,9 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.WebP.Filters; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; @@ -25,6 +27,8 @@ namespace SixLabors.ImageSharp.Formats.WebP private static readonly int BitsSpecialMarker = 0x100; + private static readonly int NumArgbCacheRows = 16; + private static readonly uint PackedNonLiteralCode = 0; private static readonly int CodeToPlaneCodes = WebPLookupTables.CodeToPlane.Length; @@ -82,22 +86,18 @@ namespace SixLabors.ImageSharp.Formats.WebP public void Decode(Buffer2D pixels, int width, int height) where TPixel : struct, IPixel { - var decoder = new Vp8LDecoder(width, height); - IMemoryOwner pixelData = this.DecodeImageStream(decoder, width, height, true); - this.DecodePixelValues(decoder, pixelData.GetSpan(), pixels); - - // Free up allocated memory. - pixelData.Dispose(); - foreach (Vp8LTransform transform in decoder.Transforms) + using (var decoder = new Vp8LDecoder(width, height, this.memoryAllocator)) { - transform.Data?.Dispose(); + this.DecodeImageStream(decoder, width, height, true); + this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); + this.DecodePixelValues(decoder, pixels); } - - decoder.Metadata?.HuffmanImage?.Dispose(); } - private IMemoryOwner DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0) + public IMemoryOwner DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0) { + int transformXSize = xSize; + int transformYSize = ySize; int numberOfTransformsPresent = 0; if (isLevel0) { @@ -111,7 +111,12 @@ namespace SixLabors.ImageSharp.Formats.WebP WebPThrowHelper.ThrowImageFormatException($"The maximum number of transforms of {WebPConstants.MaxNumberOfTransforms} was exceeded"); } - this.ReadTransformation(xSize, ySize, decoder); + this.ReadTransformation(transformXSize, transformYSize, decoder); + if (decoder.Transforms[numberOfTransformsPresent].TransformType == Vp8LTransformType.ColorIndexingTransform) + { + transformXSize = LosslessUtils.SubSampleSize(transformXSize, decoder.Transforms[numberOfTransformsPresent].Bits); + } + numberOfTransformsPresent++; } } @@ -135,14 +140,13 @@ namespace SixLabors.ImageSharp.Formats.WebP } // Read the Huffman codes (may recurse). - this.ReadHuffmanCodes(decoder, xSize, ySize, colorCacheBits, isLevel0); + this.ReadHuffmanCodes(decoder, transformXSize, transformYSize, colorCacheBits, isLevel0); decoder.Metadata.ColorCacheSize = colorCacheSize; - // Finish setting up the color-cache - ColorCache colorCache = null; + // Finish setting up the color-cache. if (colorCachePresent) { - colorCache = new ColorCache(); + decoder.Metadata.ColorCache = new ColorCache(); colorCacheSize = 1 << colorCacheBits; decoder.Metadata.ColorCacheSize = colorCacheSize; if (!(colorCacheBits >= 1 && colorCacheBits <= WebPConstants.MaxColorCacheBits)) @@ -150,24 +154,32 @@ namespace SixLabors.ImageSharp.Formats.WebP WebPThrowHelper.ThrowImageFormatException("Invalid color cache bits found"); } - colorCache.Init(colorCacheBits); + decoder.Metadata.ColorCache.Init(colorCacheBits); } else { decoder.Metadata.ColorCacheSize = 0; } - this.UpdateDecoder(decoder, xSize, ySize); + this.UpdateDecoder(decoder, transformXSize, transformYSize); + if (isLevel0) + { + // level 0 complete. + return null; + } + // Use the Huffman trees to decode the LZ77 encoded data. IMemoryOwner pixelData = this.memoryAllocator.Allocate(decoder.Width * decoder.Height, AllocationOptions.Clean); - this.DecodeImageData(decoder, pixelData.GetSpan(), colorCacheSize, colorCache); + this.DecodeImageData(decoder, pixelData.GetSpan()); return pixelData; } - private void DecodePixelValues(Vp8LDecoder decoder, Span pixelData, Buffer2D pixels) + private void DecodePixelValues(Vp8LDecoder decoder, Buffer2D pixels) where TPixel : struct, IPixel { + Span pixelData = decoder.Pixels.GetSpan(); + // Apply reverse transformations, if any are present. this.ApplyInverseTransforms(decoder, pixelData); @@ -189,7 +201,7 @@ namespace SixLabors.ImageSharp.Formats.WebP } } - private void DecodeImageData(Vp8LDecoder decoder, Span pixelData, int colorCacheSize, ColorCache colorCache) + private void DecodeImageData(Vp8LDecoder decoder, Span pixelData) { int lastPixel = 0; int width = decoder.Width; @@ -197,6 +209,8 @@ namespace SixLabors.ImageSharp.Formats.WebP int row = lastPixel / width; int col = lastPixel % width; int lenCodeLimit = WebPConstants.NumLiteralCodes + WebPConstants.NumLengthCodes; + int colorCacheSize = decoder.Metadata.ColorCacheSize; + ColorCache colorCache = decoder.Metadata.ColorCache; int colorCacheLimit = lenCodeLimit + colorCacheSize; int mask = decoder.Metadata.HuffmanMask; HTreeGroup[] hTreeGroup = this.GetHTreeGroupForPos(decoder.Metadata, col, row); @@ -207,7 +221,7 @@ namespace SixLabors.ImageSharp.Formats.WebP while (decodedPixels < totalPixels) { int code; - if ((col & mask) == 0) + if ((col & mask) is 0) { hTreeGroup = this.GetHTreeGroupForPos(decoder.Metadata, col, row); } @@ -621,7 +635,6 @@ namespace SixLabors.ImageSharp.Formats.WebP : (numColors > 4) ? 1 : (numColors > 2) ? 2 : 3; - transform.XSize = LosslessUtils.SubSampleSize(transform.XSize, bits); transform.Bits = bits; using (IMemoryOwner colorMap = this.DecodeImageStream(decoder, (int)numColors, 1, false)) { @@ -683,6 +696,208 @@ namespace SixLabors.ImageSharp.Formats.WebP } } + public void DecodeAlphaData(AlphaDecoder dec) + { + Span pixelData = dec.Vp8LDec.Pixels.Memory.Span; + Span data = MemoryMarshal.Cast(pixelData); + int row = 0; + int col = 0; + Vp8LDecoder vp8LDec = dec.Vp8LDec; + int width = vp8LDec.Width; + int height = vp8LDec.Height; + Vp8LMetadata hdr = vp8LDec.Metadata; + int pos = 0; // Current position. + int end = width * height; // End of data. + int last = end; // Last pixel to decode. + int lastRow = height; + int lenCodeLimit = WebPConstants.NumLiteralCodes + WebPConstants.NumLengthCodes; + int mask = hdr.HuffmanMask; + HTreeGroup[] htreeGroup = (pos < last) ? this.GetHTreeGroupForPos(hdr, col, row) : null; + while (!this.bitReader.Eos && pos < last) + { + // Only update when changing tile. + if ((col & mask) is 0) + { + htreeGroup = this.GetHTreeGroupForPos(hdr, col, row); + } + + this.bitReader.FillBitWindow(); + int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]); + if (code < WebPConstants.NumLiteralCodes) + { + // Literal + data[pos] = (byte)code; + ++pos; + ++col; + + if (col >= width) + { + col = 0; + ++row; + if (row <= lastRow && (row % NumArgbCacheRows is 0)) + { + this.ExtractPalettedAlphaRows(dec, row); + } + } + } + else if (code < lenCodeLimit) + { + // Backward reference + int lengthSym = code - WebPConstants.NumLiteralCodes; + int length = this.GetCopyLength(lengthSym); + int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); + this.bitReader.FillBitWindow(); + int distCode = this.GetCopyDistance(distSymbol); + int dist = this.PlaneCodeToDistance(width, distCode); + if (pos >= dist && end - pos >= length) + { + //CopyBlock8b(data + pos, dist, length); + } + else + { + // TODO: error? + break; + } + + pos += length; + col += length; + while (col >= width) + { + col -= width; + ++row; + if (row <= lastRow && (row % NumArgbCacheRows is 0)) + { + this.ExtractPalettedAlphaRows(dec, row); + } + } + + if (pos < last && (col & mask) > 0) + { + htreeGroup = this.GetHTreeGroupForPos(hdr, col, row); + } + } + else + { + WebPThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + } + + this.bitReader.Eos = this.bitReader.IsEndOfStream(); + } + + // Process the remaining rows corresponding to last row-block. + this.ExtractPalettedAlphaRows(dec, row > lastRow ? lastRow : row); + } + + private void ExtractPalettedAlphaRows(AlphaDecoder dec, int lastRow) + { + // For vertical and gradient filtering, we need to decode the part above the + // cropTop row, in order to have the correct spatial predictors. + int topRow = (dec.FilterType is WebPFilterType.None || dec.FilterType is WebPFilterType.Horizontal) + ? dec.CropTop + : dec.LastRow; + int firstRow = (dec.LastRow < topRow) ? topRow : dec.LastRow; + if (lastRow > firstRow) + { + // Special method for paletted alpha data. We only process the cropped area. + Span output = dec.Alpha.AsSpan(); + Span pixelData = dec.Vp8LDec.Pixels.Memory.Span; + Span pixelDataAsBytes = MemoryMarshal.Cast(pixelData); + Span dst = output.Slice(dec.Width * firstRow); + Span input = pixelDataAsBytes.Slice(dec.Vp8LDec.Width * firstRow); + + // TODO: check if any and the correct transform is present + Vp8LTransform transform = dec.Vp8LDec.Transforms[0]; + this.ColorIndexInverseTransformAlpha(transform, firstRow, lastRow, input, dst); + //dec.AlphaApplyFilter(firstRow, lastRow, dst, width); + } + + dec.LastRow = lastRow; + } + + private void ColorIndexInverseTransformAlpha( + Vp8LTransform transform, + int yStart, + int yEnd, + Span src, + Span dst) + { + int bitsPerPixel = 8 >> transform.Bits; + int width = transform.XSize; + Span colorMap = transform.Data.Memory.Span; + int srcOffset = 0; + int dstOffset = 0; + if (bitsPerPixel < 8) + { + int pixelsPerByte = 1 << transform.Bits; + int countMask = pixelsPerByte - 1; + int bitMask = (1 << bitsPerPixel) - 1; + for (int y = yStart; y < yEnd; ++y) + { + int packedPixels = 0; + for (int x = 0; x < width; ++x) + { + if ((x & countMask) is 0) + { + packedPixels = src[srcOffset]; + srcOffset++; + } + + dst[dstOffset] = GetAlphaValue((int)colorMap[packedPixels & bitMask]); + dstOffset++; + packedPixels >>= bitsPerPixel; + } + } + } + else + { + MapAlpha(src, colorMap, dst, yStart, yEnd, width); + } + } + + private static void MapAlpha(Span src, Span colorMap, Span dst, int yStart, int yEnd, int width) + { + int offset = 0; + for (int y = yStart; y < yEnd; ++y) + { + for (int x = 0; x < width; ++x) + { + dst[offset] = GetAlphaValue((int)colorMap[src[offset]]); + offset++; + } + } + } + + private static bool Is8bOptimizable(Vp8LMetadata hdr) + { + if (hdr.ColorCacheSize > 0) + { + return false; + } + + // When the Huffman tree contains only one symbol, we can skip the + // call to ReadSymbol() for red/blue/alpha channels. + for (int i = 0; i < hdr.NumHTreeGroups; ++i) + { + List htrees = hdr.HTreeGroups[i].HTrees; + if (htrees[HuffIndex.Red][0].Value > 0) + { + return false; + } + + if (htrees[HuffIndex.Blue][0].Value > 0) + { + return false; + } + + if (htrees[HuffIndex.Alpha][0].Value > 0) + { + return false; + } + } + + return true; + } + private void UpdateDecoder(Vp8LDecoder decoder, int width, int height) { int numBits = decoder.Metadata.HuffmanSubSampleBits; @@ -752,12 +967,11 @@ namespace SixLabors.ImageSharp.Formats.WebP } /// - /// Decodes the next Huffman code from bit-stream. + /// Decodes the next Huffman code from the bit-stream. /// FillBitWindow(br) needs to be called at minimum every second call to ReadSymbol, in order to pre-fetch enough bits. /// private uint ReadSymbol(Span table) { - // TODO: if the bitReader field is moved to this base class we could omit the parameter. uint val = (uint)this.bitReader.PrefetchBits(); Span tableSpan = table.Slice((int)(val & HuffmanUtils.HuffmanTableMask)); int nBits = tableSpan[0].BitsUsed - HuffmanUtils.HuffmanTableBits; @@ -832,5 +1046,10 @@ namespace SixLabors.ImageSharp.Formats.WebP huff.Value |= hCode.Value << shift; return hCode.BitsUsed; } + + private static byte GetAlphaValue(int val) + { + return (byte)((val >> 8) & 0xff); + } } } diff --git a/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs b/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs index 34ddade1e..9c906a828 100644 --- a/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs +++ b/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; @@ -66,20 +65,19 @@ namespace SixLabors.ImageSharp.Formats.WebP // Decode image data. this.ParseFrame(decoder, io); - byte[] decodedAlpha = null; if (info.Features?.Alpha is true) { - var alphaDecoder = new AlphaDecoder(width, height, info.Features.AlphaData); - - // TODO: use memory allocator. - decodedAlpha = new byte[width * height]; - alphaDecoder.Decode(decoder, decodedAlpha.AsSpan()); + var alphaDecoder = new AlphaDecoder(width, height, info.Features.AlphaData, info.Features.AlphaChunkHeader, this.memoryAllocator); + alphaDecoder.Decode(); + this.DecodePixelValues(width, height, decoder.Pixels, pixels, alphaDecoder.Alpha); + } + else + { + this.DecodePixelValues(width, height, decoder.Pixels, pixels); } - - this.DecodePixelValues(width, height, decoder.Bgr, decodedAlpha, pixels); } - private void DecodePixelValues(int width, int height, Span pixelData, byte[] alpha, Buffer2D pixels) + private void DecodePixelValues(int width, int height, Span pixelData, Buffer2D pixels, byte[] alpha = null) where TPixel : struct, IPixel { TPixel color = default; @@ -628,7 +626,7 @@ namespace SixLabors.ImageSharp.Formats.WebP private int EmitRgb(Vp8Decoder dec, Vp8Io io) { - byte[] buf = dec.Bgr; + byte[] buf = dec.Pixels; int numLinesOut = io.MbH; // a priori guess. Span curY = io.Y; Span curU = io.U; @@ -1359,37 +1357,6 @@ namespace SixLabors.ImageSharp.Formats.WebP return io; } - private static bool Is8bOptimizable(Vp8LMetadata hdr) - { - if (hdr.ColorCacheSize > 0) - { - return false; - } - - // When the Huffman tree contains only one symbol, we can skip the - // call to ReadSymbol() for red/blue/alpha channels. - for (int i = 0; i < hdr.NumHTreeGroups; ++i) - { - List htrees = hdr.HTreeGroups[i].HTrees; - if (htrees[HuffIndex.Red][0].Value > 0) - { - return false; - } - - if (htrees[HuffIndex.Blue][0].Value > 0) - { - return false; - } - - if (htrees[HuffIndex.Alpha][0].Value > 0) - { - return false; - } - } - - return true; - } - private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz) { nzCoeffs <<= 2; diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs index a6898093a..1d9c41660 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs @@ -169,11 +169,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.WebP } [Theory] - [WithFile(Lossy.Alpha1, PixelTypes.Rgba32)] - [WithFile(Lossy.Alpha2, PixelTypes.Rgba32)] - [WithFile(Lossy.Alpha3, PixelTypes.Rgba32)] - [WithFile(Lossy.Alpha4, PixelTypes.Rgba32)] [WithFile(Lossy.AlphaNoCompression, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaNoCompressionNoFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaCompressedNoFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaNoCompressionHorizontalFilter, PixelTypes.Rgba32)] public void WebpDecoder_CanDecode_Lossy_WithAlpha(TestImageProvider provider) where TPixel : struct, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e892d87af..32ee475a7 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -491,10 +491,15 @@ namespace SixLabors.ImageSharp.Tests // Lossy images with an alpha channel. public const string Alpha1 = "WebP/lossy_alpha1.webp"; public const string Alpha2 = "WebP/lossy_alpha2.webp"; - public const string Alpha3 = "WebP/lossy_alpha3.webp"; - public const string Alpha4 = "WebP/lossy_alpha4.webp"; public const string AlphaNoCompression = "WebP/alpha_no_compression.webp"; - + public const string AlphaNoCompressionNoFilter = "WebP/alpha_filter_0_method_0.webp"; + public const string AlphaCompressedNoFilter = "WebP/alpha_filter_0_method_1.webp"; + public const string AlphaNoCompressionHorizontalFilter = "WebP/alpha_filter_1_method_0.webp"; + public const string AlphaCompressedHorizontalFilter = "WebP/alpha_filter_1_method_1.webp"; + public const string AlphaNoCompressionVerticalFilter = "WebP/alpha_filter_2_method_0.webp"; + public const string AlphaCompressedVerticalFilter = "WebP/alpha_filter_2_method_1.webp"; + public const string AlphaNoCompressionGradientFilter = "WebP/alpha_filter_3_method_0.webp"; + public const string AlphaCompressedGradientFilter = "WebP/alpha_filter_3_method_1.webp"; } } }