diff --git a/src/ImageSharp/Formats/WebP/HuffmanCode.cs b/src/ImageSharp/Formats/WebP/HuffmanCode.cs
new file mode 100644
index 0000000000..a5511335d5
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/HuffmanCode.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.WebP
+{
+ internal class HuffmanCode
+ {
+ ///
+ /// Gets or sets the number of bits used for this symbol.
+ ///
+ public int BitsUsed { get; set; }
+
+ ///
+ /// Gets or sets the symbol value or table offset.
+ ///
+ public int Value { get; set; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs
index d1c4517995..7bd1f559b6 100644
--- a/src/ImageSharp/Formats/WebP/WebPConstants.cs
+++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs
@@ -27,16 +27,6 @@ namespace SixLabors.ImageSharp.Formats.WebP
0x2A
};
- ///
- /// Signature byte which identifies a VP8L header.
- ///
- public static byte Vp8LMagicByte = 0x2F;
-
- ///
- /// Bits for width and height infos of a VPL8 image.
- ///
- public static int Vp8LImageSizeBits = 14;
-
///
/// The header bytes identifying RIFF file.
///
@@ -58,5 +48,50 @@ namespace SixLabors.ImageSharp.Formats.WebP
0x42, // B
0x50 // P
};
+
+ ///
+ /// Signature byte which identifies a VP8L header.
+ ///
+ public static byte Vp8LMagicByte = 0x2F;
+
+ ///
+ /// 3 bits reserved for version.
+ ///
+ public static int Vp8LVersionBits = 3;
+
+ ///
+ /// Bits for width and height infos of a VPL8 image.
+ ///
+ public static int Vp8LImageSizeBits = 14;
+
+ ///
+ /// Maximum number of color cache bits.
+ ///
+ public static int MaxColorCacheBits = 11;
+
+ ///
+ /// The maximum number of allowed transforms in a bitstream.
+ ///
+ public static int MaxNumberOfTransforms = 4;
+
+ public static int MaxAllowedCodeLength = 15;
+
+ public static int HuffmanCodesPerMetaCode = 5;
+
+ public static int NumLiteralCodes = 256;
+
+ public static int NumLengthCodes = 24;
+
+ public static int NumDistanceCodes = 40;
+
+ public static int NumCodeLengthCodes = 19;
+
+ public static byte[] KCodeLengthCodeOrder = { 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
+
+ public static int[] kAlphabetSize = {
+ NumLiteralCodes + NumLengthCodes,
+ NumLiteralCodes, NumLiteralCodes, NumLiteralCodes,
+ NumDistanceCodes
+ };
}
}
diff --git a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs
index 3d4c8a8c0f..70cfe6ac72 100644
--- a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs
+++ b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs
@@ -80,11 +80,13 @@ namespace SixLabors.ImageSharp.Formats.WebP
Buffer2D pixels = image.GetRootFramePixelBuffer();
if (imageInfo.IsLossLess)
{
- ReadSimpleLossless(pixels, image.Width, image.Height, (int)imageInfo.ImageDataSize);
+ var losslessDecoder = new WebPLosslessDecoder(this.currentStream, (int)imageInfo.ImageDataSize);
+ losslessDecoder.Decode(pixels, image.Width, image.Height);
}
else
{
- ReadSimpleLossy(pixels, image.Width, image.Height, (int)imageInfo.ImageDataSize);
+ var lossyDecoder = new WebPLossyDecoder(this.configuration, this.currentStream);
+ lossyDecoder.Decode(pixels, image.Width, image.Height, (int)imageInfo.ImageDataSize);
}
// There can be optional chunks after the image data, like EXIF, XMP etc.
@@ -310,10 +312,10 @@ namespace SixLabors.ImageSharp.Formats.WebP
// The alpha_is_used flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise.
bool alphaIsUsed = bitReader.ReadBit();
- // The next 3 bytes are the version. The version_number is a 3 bit code that must be set to 0.
+ // The next 3 bits are the version. The version_number is a 3 bit code that must be set to 0.
// Any other value should be treated as an error.
// TODO: should we throw here when version number is != 0?
- uint version = bitReader.Read(3);
+ uint version = bitReader.Read(WebPConstants.Vp8LVersionBits);
return new WebPImageInfo()
{
@@ -328,18 +330,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
private void ReadSimpleLossy(Buffer2D pixels, int width, int height, int imageDataSize)
where TPixel : struct, IPixel
{
- var lossyDecoder = new WebPLossyDecoder(this.configuration, this.currentStream);
- lossyDecoder.Decode(pixels, width, height, imageDataSize);
- }
-
- private void ReadSimpleLossless(Buffer2D pixels, int width, int height, int imageDataSize)
- where TPixel : struct, IPixel
- {
- var losslessDecoder = new WebPLosslessDecoder(this.currentStream);
- losslessDecoder.Decode(pixels, width, height, imageDataSize);
-
- // TODO: implement decoding. For simulating the decoding: skipping the chunk size bytes.
- this.currentStream.Skip(imageDataSize + 34); // TODO: Not sure why the additional data starts at offset +34 at the moment.
+
}
private void ReadExtended(Buffer2D pixels, int width, int height)
diff --git a/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs b/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs
index 4545e58557..7a2c55b371 100644
--- a/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs
+++ b/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
+using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Memory;
@@ -11,29 +12,226 @@ namespace SixLabors.ImageSharp.Formats.WebP
///
/// Decoder for lossless webp images.
///
+ ///
+ /// The lossless specification can be found here:
+ /// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
+ ///
internal sealed class WebPLosslessDecoder
{
- private Vp8LBitReader bitReader;
+ private readonly Vp8LBitReader bitReader;
- public WebPLosslessDecoder(Stream stream)
+ private readonly int imageDataSize;
+
+ public WebPLosslessDecoder(Stream stream, int imageDataSize)
{
this.bitReader = new Vp8LBitReader(stream);
+ this.imageDataSize = imageDataSize;
+
+ // TODO: implement decoding. For simulating the decoding: skipping the chunk size bytes.
+ //stream.Skip(imageDataSize + 34); // TODO: Not sure why the additional data starts at offset +34 at the moment.
}
- public void Decode(Buffer2D pixels, int width, int height, int imageDataSize)
+ public void Decode(Buffer2D pixels, int width, int height)
where TPixel : struct, IPixel
{
- //ReadTransformations();
+ this.ReadTransformations();
+ int xsize = 0, ysize = 0;
+ this.ReadHuffmanCodes(xsize, ysize);
+ }
+
+ private void ReadHuffmanCodes(int xsize, int ysize)
+ {
+ int maxAlphabetSize = 0;
+ int colorCacheBits = 0;
+ int numHtreeGroups = 1;
+ int numHtreeGroupsMax = 1;
+
+ // Read color cache, if present.
+ bool colorCachePresent = this.bitReader.ReadBit();
+ if (colorCachePresent)
+ {
+ colorCacheBits = (int)this.bitReader.Read(4);
+ int colorCacheSize = 1 << colorCacheBits;
+ if (!(colorCacheBits >= 1 && colorCacheBits <= WebPConstants.MaxColorCacheBits))
+ {
+ WebPThrowHelper.ThrowImageFormatException("Invalid color cache bits found");
+ }
+ }
+
+ // Read the Huffman codes.
+ // If the next bit is zero, there is only one meta Huffman code used everywhere in the image. No more data is stored.
+ // If this bit is one, the image uses multiple meta Huffman codes. These meta Huffman codes are stored as an entropy image.
+ bool isEntropyImage = this.bitReader.ReadBit();
+ if (isEntropyImage)
+ {
+ uint huffmanPrecision = this.bitReader.Read(3) + 2;
+ int huffmanXSize = SubSampleSize(xsize, (int)huffmanPrecision);
+ int huffmanYSize = SubSampleSize(ysize, (int)huffmanPrecision);
+
+ // TODO: decode entropy image
+ return;
+ }
+
+ // Find maximum alphabet size for the htree group.
+ for (int j = 0; j < WebPConstants.HuffmanCodesPerMetaCode; j++)
+ {
+ int alphabetSize = WebPConstants.kAlphabetSize[j];
+ if (j == 0 && colorCacheBits > 0)
+ {
+ alphabetSize += 1 << colorCacheBits;
+ }
+
+ if (maxAlphabetSize < alphabetSize)
+ {
+ maxAlphabetSize = alphabetSize;
+ }
+ }
+
+ for (int i = 0; i < numHtreeGroupsMax; i++)
+ {
+ int size;
+ int totalSize = 0;
+ int isTrivialLiteral = 1;
+ int maxBits = 0;
+ var codeLengths = new int[maxAlphabetSize];
+ for (int j = 0; j < WebPConstants.HuffmanCodesPerMetaCode; j++)
+ {
+ int alphabetSize = WebPConstants.kAlphabetSize[j];
+ if (j == 0 && colorCacheBits > 0)
+ {
+ alphabetSize += 1 << colorCacheBits;
+ size = this.ReadHuffmanCode(alphabetSize, codeLengths);
+ if (size is 0)
+ {
+ WebPThrowHelper.ThrowImageFormatException("Huffman table size is zero");
+ }
+ }
+ }
+ }
+ }
+
+ private int ReadHuffmanCode(int alphabetSize, int[] codeLengths)
+ {
+ bool simpleCode = this.bitReader.ReadBit();
+ if (simpleCode)
+ {
+ // (i) Simple Code Length Code.
+ // This variant is used in the special case when only 1 or 2 Huffman code lengths are non - zero,
+ // and are in the range of[0, 255].All other Huffman code lengths are implicitly zeros.
+
+ // Read symbols, codes & code lengths directly.
+ uint numSymbols = this.bitReader.Read(1) + 1;
+ uint firstSymbolLenCode = this.bitReader.Read(1);
+
+ // The first code is either 1 bit or 8 bit code.
+ uint symbol = this.bitReader.Read((firstSymbolLenCode == 0) ? 1 : 8);
+ codeLengths[symbol] = 1;
+
+ // The second code (if present), is always 8 bit long.
+ if (numSymbols == 2)
+ {
+ symbol = this.bitReader.Read(8);
+ codeLengths[symbol] = 1;
+ }
+ }
+ else
+ {
+ // (ii)Normal Code Length Code:
+ // The code lengths of a Huffman code are read as follows: num_code_lengths specifies the number of code lengths;
+ // the rest of the code lengths (according to the order in kCodeLengthCodeOrder) are zeros.
+ var codeLengthCodeLengths = new int[WebPConstants.NumLengthCodes];
+ uint numCodes = this.bitReader.Read(4) + 4;
+ if (numCodes > WebPConstants.NumCodeLengthCodes)
+ {
+ WebPThrowHelper.ThrowImageFormatException("Bitstream error, numCodes has an invalid value");
+ }
+
+ for (int i = 0; i < numCodes; i++)
+ {
+ codeLengthCodeLengths[WebPConstants.KCodeLengthCodeOrder[i]] = (int)this.bitReader.Read(3);
+ }
+
+ // TODO: ReadHuffmanCodeLengths
+ }
+
+ int size = 0;
+ // TODO: VP8LBuildHuffmanTable
+
+ return size;
+ }
+
+ private int BuildHuffmanTable(int rootBits, int[] codeLengths, int codeLengthsSize,
+ int[] sorted) // sorted[code_lengths_size] is a pre-allocated array for sorting symbols by code length.
+ {
+ // total size root table + 2nd level table
+ int totalSize = 1 << rootBits;
+ // current code length
+ int len;
+ // symbol index in original or sorted table
+ int symbol;
+ // number of codes of each length:
+ var count = new int[WebPConstants.MaxAllowedCodeLength + 1];
+ // offsets in sorted table for each length
+ var offset = new int[WebPConstants.MaxAllowedCodeLength + 1];
+
+ // Build histogram of code lengths.
+ for (symbol = 0; symbol < codeLengthsSize; ++symbol)
+ {
+ if (codeLengths[symbol] > WebPConstants.MaxAllowedCodeLength)
+ {
+ return 0;
+ }
+
+ ++count[codeLengths[symbol]];
+ }
+
+ // Generate offsets into sorted symbol table by code length.
+ offset[1] = 0;
+ for (len = 1; len < WebPConstants.MaxAllowedCodeLength; ++len)
+ {
+ if (count[len] > (1 << len))
+ {
+ return 0;
+ }
+
+ offset[len + 1] = offset[len] + count[len];
+ }
+
+ // Sort symbols by length, by symbol order within each length.
+ for (symbol = 0; symbol < codeLengthsSize; ++symbol)
+ {
+ int symbolCodeLength = codeLengths[symbol];
+ if (codeLengths[symbol] > 0)
+ {
+ sorted[offset[symbolCodeLength]++] = symbol;
+ }
+ }
+
+ // Special case code with only one value.
+ if (offset[WebPConstants.MaxAllowedCodeLength] is 1)
+ {
+ var huffmanCode = new HuffmanCode()
+ {
+ BitsUsed = 0,
+ Value = sorted[0]
+ };
+
+ return totalSize;
+ }
+
+ return 0;
}
private void ReadTransformations()
{
// Next bit indicates, if a transformation is present.
- bool transformPresent = bitReader.ReadBit();
+ bool transformPresent = this.bitReader.ReadBit();
int numberOfTransformsPresent = 0;
+ var transforms = new List(WebPConstants.MaxNumberOfTransforms);
while (transformPresent)
{
- var transformType = (WebPTransformType)bitReader.Read(2);
+ var transformType = (WebPTransformType)this.bitReader.Read(2);
+ transforms.Add(transformType);
switch (transformType)
{
case WebPTransformType.SubtractGreen:
@@ -42,7 +240,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
case WebPTransformType.ColorIndexingTransform:
// The transform data contains color table size and the entries in the color table.
// 8 bit value for color table size.
- uint colorTableSize = bitReader.Read(8) + 1;
+ uint colorTableSize = this.bitReader.Read(8) + 1;
// TODO: color table should follow here?
break;
@@ -51,7 +249,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
{
// The first 3 bits of prediction data define the block width and height in number of bits.
// The number of block columns, block_xsize, is used in indexing two-dimensionally.
- uint sizeBits = bitReader.Read(3) + 2;
+ uint sizeBits = this.bitReader.Read(3) + 2;
int blockWidth = 1 << (int)sizeBits;
int blockHeight = 1 << (int)sizeBits;
@@ -62,7 +260,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
{
// The first 3 bits of the color transform data contain the width and height of the image block in number of bits,
// just like the predictor transform:
- uint sizeBits = bitReader.Read(3) + 2;
+ uint sizeBits = this.bitReader.Read(3) + 2;
int blockWidth = 1 << (int)sizeBits;
int blockHeight = 1 << (int)sizeBits;
break;
@@ -70,15 +268,23 @@ namespace SixLabors.ImageSharp.Formats.WebP
}
numberOfTransformsPresent++;
- if (numberOfTransformsPresent == 4)
+
+ transformPresent = this.bitReader.ReadBit();
+ if (numberOfTransformsPresent == WebPConstants.MaxNumberOfTransforms && transformPresent)
{
- break;
+ WebPThrowHelper.ThrowImageFormatException("The maximum number of transforms was exceeded");
}
-
- transformPresent = bitReader.ReadBit();
}
// TODO: return transformation in an appropriate form.
}
+
+ ///
+ /// Computes sampled size of 'size' when sampling using 'sampling bits'.
+ ///
+ private int SubSampleSize(int size, int samplingBits)
+ {
+ return (size + (1 << samplingBits) - 1) >> samplingBits;
+ }
}
}