diff --git a/src/ImageSharp/Formats/WebP/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/BackwardReferenceEncoder.cs
new file mode 100644
index 0000000000..770cce5ec0
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/BackwardReferenceEncoder.cs
@@ -0,0 +1,661 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal class BackwardReferenceEncoder
+ {
+ private const int HashBits = 18;
+
+ private const int HashSize = 1 << HashBits;
+
+ private const uint HashMultiplierHi = 0xc6a4a793u;
+
+ private const uint HashMultiplierLo = 0x5bd1e996u;
+
+ private const float MaxEntropy = 1e30f;
+
+ private const int WindowOffsetsSizeMax = 32;
+
+ ///
+ /// Minimum block size for backward references.
+ ///
+ private const int MinBlockSize = 256;
+
+ ///
+ /// The number of bits for the window size.
+ ///
+ private const int WindowSizeBits = 20;
+
+ ///
+ /// 1M window (4M bytes) minus 120 special codes for short distances.
+ ///
+ private const int WindowSize = (1 << WindowSizeBits) - 120;
+
+ ///
+ /// Maximum bit length.
+ ///
+ private const int MaxLengthBits = 12;
+
+ ///
+ /// We want the max value to be attainable and stored in MaxLengthBits bits.
+ ///
+ private const int MaxLength = (1 << MaxLengthBits) - 1;
+
+ ///
+ /// Minimum number of pixels for which it is cheaper to encode a
+ /// distance + length instead of each pixel as a literal.
+ ///
+ private const int MinLength = 4;
+
+ public static void HashChainFill(Vp8LHashChain p, Span bgra, int quality, int xSize, int ySize)
+ {
+ int size = xSize * ySize;
+ int iterMax = GetMaxItersForQuality(quality);
+ int windowSize = GetWindowSizeForHashChain(quality, xSize);
+ int pos;
+ var hashToFirstIndex = new int[HashSize]; // TODO: use memory allocator
+
+ // Initialize hashToFirstIndex array to -1.
+ hashToFirstIndex.AsSpan().Fill(-1);
+
+ var chain = new int[size]; // TODO: use memory allocator.
+
+ // Fill the chain linking pixels with the same hash.
+ var bgraComp = bgra[0] == bgra[1];
+ for (pos = 0; pos < size - 2;)
+ {
+ uint hashCode;
+ bool bgraCompNext = bgra[pos + 1] == bgra[pos + 2];
+ if (bgraComp && bgraCompNext)
+ {
+ // Consecutive pixels with the same color will share the same hash.
+ // We therefore use a different hash: the color and its repetition length.
+ var tmp = new uint[2];
+ uint len = 1;
+ tmp[0] = bgra[pos];
+
+ // Figure out how far the pixels are the same. The last pixel has a different 64 bit hash,
+ // as its next pixel does not have the same color, so we just need to get to
+ // the last pixel equal to its follower.
+ while (pos + (int)len + 2 < size && bgra[(int)(pos + len + 2)] == bgra[pos])
+ {
+ ++len;
+ }
+
+ if (len > MaxLength)
+ {
+ // Skip the pixels that match for distance=1 and length>MaxLength
+ // because they are linked to their predecessor and we automatically
+ // check that in the main for loop below. Skipping means setting no
+ // predecessor in the chain, hence -1.
+ pos += (int)(len - MaxLength);
+ len = MaxLength;
+ }
+
+ // Process the rest of the hash chain.
+ while (len > 0)
+ {
+ tmp[1] = len--;
+ hashCode = GetPixPairHash64(tmp);
+ chain[pos] = hashToFirstIndex[hashCode];
+ hashToFirstIndex[hashCode] = pos++;
+ }
+
+ bgraComp = false;
+ }
+ else
+ {
+ // Just move one pixel forward.
+ hashCode = GetPixPairHash64(bgra.Slice(pos));
+ chain[pos] = hashToFirstIndex[hashCode];
+ hashToFirstIndex[hashCode] = pos++;
+ bgraComp = bgraCompNext;
+ }
+ }
+
+ // Process the penultimate pixel.
+ chain[pos] = hashToFirstIndex[GetPixPairHash64(bgra.Slice(pos))];
+
+ // Find the best match interval at each pixel, defined by an offset to the
+ // pixel and a length. The right-most pixel cannot match anything to the right
+ // (hence a best length of 0) and the left-most pixel nothing to the left
+ // (hence an offset of 0).
+ p.OffsetLength[0] = p.OffsetLength[size - 1] = 0;
+ for (int basePosition = size - 2; basePosition > 0;)
+ {
+ int maxLen = MaxFindCopyLength(size - 1 - basePosition);
+ int bgraStart = basePosition;
+ int iter = iterMax;
+ int bestLength = 0;
+ uint bestDistance = 0;
+ uint bestBgra;
+ int minPos = (basePosition > windowSize) ? basePosition - windowSize : 0;
+ int lengthMax = (maxLen < 256) ? maxLen : 256;
+ uint maxBasePosition;
+ pos = (int)chain[basePosition];
+ int currLength;
+
+ // Heuristic: use the comparison with the above line as an initialization.
+ if (basePosition >= (uint)xSize)
+ {
+ currLength = FindMatchLength(bgra.Slice(bgraStart - xSize), bgra.Slice(bgraStart), bestLength, maxLen);
+ if (currLength > bestLength)
+ {
+ bestLength = currLength;
+ bestDistance = (uint)xSize;
+ }
+
+ iter--;
+ }
+
+ // Heuristic: compare to the previous pixel.
+ currLength = FindMatchLength(bgra.Slice(bgraStart - 1), bgra.Slice(bgraStart), bestLength, maxLen);
+ if (currLength > bestLength)
+ {
+ bestLength = currLength;
+ bestDistance = 1;
+ }
+
+ iter--;
+
+ if (bestLength == MaxLength)
+ {
+ pos = minPos - 1;
+ }
+
+ bestBgra = bgra.Slice(bgraStart)[bestLength];
+
+ for (; pos >= minPos && (--iter > 0); pos = chain[pos])
+ {
+ if (bgra[pos + bestLength] != bestBgra)
+ {
+ continue;
+ }
+
+ currLength = VectorMismatch(bgra.Slice(pos), bgra.Slice(bgraStart), maxLen);
+ if (bestLength < currLength)
+ {
+ bestLength = currLength;
+ bestDistance = (uint)(basePosition - pos);
+ bestBgra = bgra.Slice(bgraStart)[bestLength];
+
+ // Stop if we have reached a good enough length.
+ if (bestLength >= lengthMax)
+ {
+ break;
+ }
+ }
+ }
+
+ // We have the best match but in case the two intervals continue matching
+ // to the left, we have the best matches for the left-extended pixels.
+ maxBasePosition = (uint)basePosition;
+ while (true)
+ {
+ p.OffsetLength[basePosition] = (bestDistance << MaxLengthBits) | (uint)bestLength;
+ --basePosition;
+
+ // Stop if we don't have a match or if we are out of bounds.
+ if (bestDistance == 0 || basePosition == 0)
+ {
+ break;
+ }
+
+ // Stop if we cannot extend the matching intervals to the left.
+ if (basePosition < bestDistance || bgra[(int)(basePosition - bestDistance)] != bgra[basePosition])
+ {
+ break;
+ }
+
+ // Stop if we are matching at its limit because there could be a closer
+ // matching interval with the same maximum length. Then again, if the
+ // matching interval is as close as possible (best_distance == 1), we will
+ // never find anything better so let's continue.
+ if (bestLength == MaxLength && bestDistance != 1 && basePosition + MaxLength < maxBasePosition)
+ {
+ break;
+ }
+
+ if (bestLength < MaxLength)
+ {
+ bestLength++;
+ maxBasePosition = (uint)basePosition;
+ }
+ }
+ }
+
+ int foo = 0;
+ }
+
+ ///
+ /// Evaluates best possible backward references for specified quality.
+ /// The input cache_bits to 'VP8LGetBackwardReferences' sets the maximum cache
+ /// bits to use (passing 0 implies disabling the local color cache).
+ /// The optimal cache bits is evaluated and set for the *cache_bits parameter.
+ /// The return value is the pointer to the best of the two backward refs viz,
+ /// refs[0] or refs[1].
+ ///
+ private static Vp8LBackwardRefs[] GetBackwardReferences(int width, int height, uint[] bgra, int quality,
+ int lz77TypesToTry, int[] cacheBits, Vp8LHashChain[] hashChain, Vp8LBackwardRefs[] best, Vp8LBackwardRefs[] worst)
+ {
+ var histo = new Vp8LHistogram[WebPConstants.MaxColorCacheBits];
+ int lz77Type = 0;
+ int lz77TypeBest = 0;
+ double bitCostBest = -1;
+ int[] cacheBitsInitial = cacheBits;
+ // TODO: var hashChainBox = new Vp8LHashChain();
+ for (lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1)
+ {
+ int res = 0;
+ double bitCost;
+ int[] cacheBitsTmp = cacheBitsInitial;
+ if ((lz77TypesToTry & lz77Type) == 0)
+ {
+ continue;
+ }
+
+ switch ((Vp8LLz77Type)lz77Type)
+ {
+ case Vp8LLz77Type.Lz77Rle:
+ BackwardReferencesRle(width, height, bgra, 0, worst);
+ break;
+ case Vp8LLz77Type.Lz77Standard:
+ // Compute LZ77 with no cache (0 bits), as the ideal LZ77 with a color
+ // cache is not that different in practice.
+ BackwardReferencesLz77(width, height, bgra, 0, hashChain, worst);
+ break;
+ case Vp8LLz77Type.Lz77Box:
+ // TODO: HashChainInit(hashChainBox, width * height);
+ //BackwardReferencesLz77Box(width, height, bgra, 0, hashChain, hashChainBox, worst);
+ break;
+ }
+
+ // Next, try with a color cache and update the references.
+ CalculateBestCacheSize(bgra, quality, worst, cacheBitsTmp);
+ if (cacheBitsTmp[0] > 0)
+ {
+ BackwardRefsWithLocalCache(bgra, cacheBitsTmp[0], worst);
+ }
+
+ // Keep the best backward references.
+ // TODO: VP8LHistogramCreate(histo, worst, cacheBitsTmp);
+ bitCost = histo[0].EstimateBits();
+
+ if (lz77TypeBest == 0 || bitCost < bitCostBest)
+ {
+ Vp8LBackwardRefs[] tmp = worst;
+ worst = best;
+ best = tmp;
+ bitCostBest = bitCost;
+ //*cacheBits = cacheBitsTmp;
+ lz77TypeBest = lz77Type;
+ }
+ }
+
+ // Improve on simple LZ77 but only for high quality (TraceBackwards is costly).
+ if ((lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard || lz77TypeBest == (int)Vp8LLz77Type.Lz77Box) && quality >= 25)
+ {
+ /*HashChain[] hashChainTmp = (lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard) ? hashChain : hashChainBox;
+ if (BackwardReferencesTraceBackwards(width, height, bgra, cacheBits, hashChainTmp, best, worst))
+ {
+ double bitCostTrace;
+ //HistogramCreate(histo, worst, cacheBits);
+ bitCostTrace = histo[0].EstimateBits();
+ if (bitCostTrace < bitCostBest)
+ {
+ best = worst;
+ }
+ }*/
+ }
+
+ BackwardReferences2DLocality(width, best);
+
+ return best;
+ }
+
+ ///
+ /// Evaluate optimal cache bits for the local color cache.
+ /// The input *best_cache_bits sets the maximum cache bits to use (passing 0
+ /// implies disabling the local color cache). The local color cache is also
+ /// disabled for the lower (<= 25) quality.
+ ///
+ private static void CalculateBestCacheSize(uint[] bgra, int quality, Vp8LBackwardRefs[] refs, int[] bestCacheBits)
+ {
+ int cacheBitsMax = (quality <= 25) ? 0 : bestCacheBits[0];
+ double entropyMin = MaxEntropy;
+ var ccInit = new int[WebPConstants.MaxColorCacheBits + 1];
+ var hashers = new ColorCache[WebPConstants.MaxColorCacheBits + 1];
+ var c = new Vp8LRefsCursor(refs);
+ var histos = new Vp8LHistogram[WebPConstants.MaxColorCacheBits + 1];
+ if (cacheBitsMax == 0)
+ {
+ // Local color cache is disabled.
+ bestCacheBits[0] = 0;
+ return;
+ }
+
+ // Find the cache_bits giving the lowest entropy. The search is done in a
+ // brute-force way as the function (entropy w.r.t cache_bits) can be anything in practice.
+ //while (VP8LRefsCursorOk(&c))
+ /*while (true)
+ {
+ //PixOrCopy[] v = c.cur_pos;
+ if (v.IsLiteral())
+ {
+ uint pix = *bgra++;
+ uint a = (pix >> 24) & 0xff;
+ uint r = (pix >> 16) & 0xff;
+ uint g = (pix >> 8) & 0xff;
+ uint b = (pix >> 0) & 0xff;
+
+ // The keys of the caches can be derived from the longest one.
+ int key = HashPix(pix, 32 - cacheBitsMax);
+
+ // Do not use the color cache for cache_bits = 0.
+ ++histos[0].blue[b];
+ ++histos[0].literal[g];
+ ++histos[0].red[r];
+ ++histos[0].alpha[a];
+
+ // Deal with cache_bits > 0.
+ for (int i = cacheBitsMax; i >= 1; --i, key >>= 1)
+ {
+ if (VP8LColorCacheLookup(hashers[i], key) == pix)
+ {
+ ++histos[i]->literal[WebPConstants.NumLiteralCodes + WebPConstants.CodeLengthCodes + key];
+ }
+ else
+ {
+ VP8LColorCacheSet(hashers[i], key, pix);
+ ++histos[i].blue[b];
+ ++histos[i].literal[g];
+ ++histos[i].red[r];
+ ++histos[i].alpha[a];
+ }
+ }
+ }
+ else
+ {
+ // We should compute the contribution of the (distance,length)
+ // histograms but those are the same independently from the cache size.
+ // As those constant contributions are in the end added to the other
+ // histogram contributions, we can safely ignore them.
+
+ }
+ }*/
+ }
+
+ private static void BackwardReferencesTraceBackwards()
+ {
+
+ }
+
+ private static void BackwardReferencesLz77(int xSize, int ySize, uint[] bgra, int cacheBits, Vp8LHashChain[] hashChain, Vp8LBackwardRefs[] refs)
+ {
+ int iLastCheck = -1;
+ int ccInit = 0;
+ bool useColorCache = cacheBits > 0;
+ int pixCount = xSize * ySize;
+ var hashers = new ColorCache();
+ if (useColorCache)
+ {
+ hashers.Init(cacheBits);
+ }
+
+ // TODO: VP8LClearBackwardRefs(refs);
+ for (int i = 0; i < pixCount;)
+ {
+ // Alternative #1: Code the pixels starting at 'i' using backward reference.
+ int offset = 0;
+ int len = 0;
+ int j;
+ // TODO: VP8LHashChainFindCopy(hashChain, i, offset, ref len);
+ if (len >= MinLength)
+ {
+ int lenIni = len;
+ int maxReach = 0;
+ int jMax = (i + lenIni >= pixCount) ? pixCount - 1 : i + lenIni;
+
+ // Only start from what we have not checked already.
+ iLastCheck = (i > iLastCheck) ? i : iLastCheck;
+
+ // We know the best match for the current pixel but we try to find the
+ // best matches for the current pixel AND the next one combined.
+ // The naive method would use the intervals:
+ // [i,i+len) + [i+len, length of best match at i+len)
+ // while we check if we can use:
+ // [i,j) (where j<=i+len) + [j, length of best match at j)
+ for (j = iLastCheck + 1; j <= jMax; j++)
+ {
+ int lenJ = 0; // TODO: HashChainFindLength(hashChain, j);
+ int reach = j + (lenJ >= MinLength ? lenJ : 1); // 1 for single literal.
+ if (reach > maxReach)
+ {
+ len = j - i;
+ maxReach = reach;
+ if (maxReach >= pixCount)
+ {
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ len = 1;
+ }
+
+ // Go with literal or backward reference.
+ /*if (len == 1)
+ {
+ AddSingleLiteral(bgra[i], useColorCache, hashers, refs);
+ }
+ else
+ {
+ VP8LBackwardRefsCursorAdd(refs, PixOrCopyCreateCopy(offset, len));
+ if (useColorCache)
+ {
+ for (j = i; j < i + len; ++j)
+ {
+ VP8LColorCacheInsert(hashers, bgra[j]);
+ }
+ }
+ }
+ */
+ i += len;
+ }
+ }
+
+ ///
+ /// Compute an LZ77 by forcing matches to happen within a given distance cost.
+ /// We therefore limit the algorithm to the lowest 32 values in the PlaneCode definition.
+ ///
+ private static void BackwardReferencesLz77Box(int xSize, int ySize, uint[] bgra, int cacheBits, Vp8LHashChain[] hashChainBest, Vp8LHashChain[] hashChain, Vp8LBackwardRefs[] refs)
+ {
+ int i;
+ int pixCount = xSize * ySize;
+ short[] counts;
+ var windowOffsets = new int[WindowOffsetsSizeMax];
+ var windowOffsetsNew = new int[WindowOffsetsSizeMax];
+ int windowOffsetsSize = 0;
+ int windowOffsetsNewSize = 0;
+ short[] countsIni = new short[xSize * ySize];
+ int bestOffsetPrev = -1;
+ int bestLengthPrev = -1;
+
+ // counts[i] counts how many times a pixel is repeated starting at position i.
+ i = pixCount - 2;
+ /*counts = countsIni + i;
+ counts[1] = 1;
+ for (; i >= 0; i--, counts--)
+ {
+ if (bgra[i] == bgra[i + 1])
+ {
+ // Max out the counts to MAX_LENGTH.
+ counts[0] = counts[1] + (counts[1] != MaxLength);
+ }
+ else
+ {
+ counts[0] = 1;
+ }
+ }*/
+ }
+
+ private static void BackwardReferencesRle(int xSize, int ySize, uint[] bgra, int cacheBits, Vp8LBackwardRefs[] refs)
+ {
+ int pixCount = xSize * ySize;
+ int i, k;
+ bool useColorCache = cacheBits > 0;
+ }
+
+ ///
+ /// Update (in-place) backward references for the specified cacheBits.
+ ///
+ private static void BackwardRefsWithLocalCache(uint[] bgra, int cacheBits, Vp8LBackwardRefs[] refs)
+ {
+ int pixelIndex = 0;
+ var c = new Vp8LRefsCursor(refs);
+ var hashers = new ColorCache();
+ hashers.Init(cacheBits);
+ //while (VP8LRefsCursorOk(&c))
+ /*while (true)
+ {
+ PixOrCopy[] v = c.curPos;
+ if (v.IsLiteral())
+ {
+ uint bgraLiteral = v.BgraOrDistance;
+ int ix = VP8LColorCacheContains(hashers, bgraLiteral);
+ if (ix >= 0)
+ {
+ // hashers contains bgraLiteral
+ v = PixOrCopyCreateCacheIdx(ix);
+ }
+ else
+ {
+ VP8LColorCacheInsert(hashers, bgraLiteral);
+ }
+
+ pixelIndex++;
+ }
+ else
+ {
+ // refs was created without local cache, so it can not have cache indexes.
+ for (int k = 0; k < v.len; k++)
+ {
+ VP8LColorCacheInsert(hashers, bgra[pixelIndex++]);
+ }
+ }
+
+ VP8LRefsCursorNext(c);
+ }
+
+ VP8LColorCacheClear(hashers);*/
+ }
+
+ private static void BackwardReferences2DLocality(int xSize, Vp8LBackwardRefs[] refs)
+ {
+ var c = new Vp8LRefsCursor(refs);
+ /*while (VP8LRefsCursorOk(&c))
+ {
+ if (c.cur_pos.IsCopy())
+ {
+ int dist = c.curPos.ArgbOrDistance;
+ int transformedDist = DistanceToPlaneCode(xSize, dist);
+ c.curPos.ArgbOrDistance = transformedDist;
+ }
+
+ VP8LRefsCursorNext(&c);
+ }*/
+ }
+
+ private static int DistanceToPlaneCode(int xSize, int dist)
+ {
+ int yOffset = dist / xSize;
+ int xOffset = dist - (yOffset * xSize);
+ if (xOffset <= 8 && yOffset < 8)
+ {
+ return (int)WebPLookupTables.PlaneToCodeLut[(yOffset * 16) + 8 - xOffset] + 1;
+ }
+ else if (xOffset > xSize - 8 && yOffset < 7)
+ {
+ return (int)WebPLookupTables.PlaneToCodeLut[((yOffset + 1) * 16) + 8 + (xSize - xOffset)] + 1;
+ }
+
+ return dist + 120;
+ }
+
+ ///
+ /// Returns the exact index where array1 and array2 are different. For an index
+ /// inferior or equal to best_len_match, the return value just has to be strictly
+ /// inferior to best_len_match. The current behavior is to return 0 if this index
+ /// is best_len_match, and the index itself otherwise.
+ /// If no two elements are the same, it returns max_limit.
+ ///
+ private static int FindMatchLength(Span array1, Span array2, int bestLenMatch, int maxLimit)
+ {
+ // Before 'expensive' linear match, check if the two arrays match at the
+ // current best length index.
+ if (array1[bestLenMatch] != array2[bestLenMatch])
+ {
+ return 0;
+ }
+
+ return VectorMismatch(array1, array2, maxLimit);
+ }
+
+ private static int VectorMismatch(Span array1, Span array2, int length)
+ {
+ int matchLen = 0;
+
+ while (matchLen < length && array1[matchLen] == array2[matchLen])
+ {
+ matchLen++;
+ }
+
+ return matchLen;
+ }
+
+ ///
+ /// Calculates the hash for a pixel pair.
+ ///
+ /// An Span with two pixels.
+ /// The hash.
+ private static uint GetPixPairHash64(Span bgra)
+ {
+ uint key = bgra[1] * HashMultiplierHi;
+ key += bgra[0] * HashMultiplierLo;
+ key = key >> (32 - HashBits);
+ return key;
+ }
+
+ ///
+ /// Returns the maximum number of hash chain lookups to do for a
+ /// given compression quality. Return value in range [8, 86].
+ ///
+ /// The quality.
+ /// Number of hash chain lookups.
+ private static int GetMaxItersForQuality(int quality)
+ {
+ return 8 + (quality * quality / 128);
+ }
+
+ private static int MaxFindCopyLength(int len)
+ {
+ return (len < MaxLength) ? len : MaxLength;
+ }
+
+ private static int GetWindowSizeForHashChain(int quality, int xSize)
+ {
+ int maxWindowSize = (quality > 75) ? WindowSize
+ : (quality > 50) ? (xSize << 8)
+ : (quality > 25) ? (xSize << 6)
+ : (xSize << 4);
+
+ return (maxWindowSize > WindowSize) ? WindowSize : maxWindowSize;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/HuffmanTree.cs b/src/ImageSharp/Formats/WebP/Lossless/HuffmanTree.cs
new file mode 100644
index 0000000000..3fba3327dd
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/HuffmanTree.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ ///
+ /// Represents the Huffman tree.
+ ///
+ internal class HuffmanTree
+ {
+ ///
+ /// Gets the symbol frequency.
+ ///
+ public int TotalCount { get; }
+
+ ///
+ /// Gets the symbol value.
+ ///
+ public int Value { get; }
+
+ ///
+ /// Gets the index for the left sub-tree.
+ ///
+ public int PoolIndexLeft { get; }
+
+ ///
+ /// Gets the index for the right sub-tree.
+ ///
+ public int PoolIndexRight { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/HuffmanTreeCode.cs b/src/ImageSharp/Formats/WebP/Lossless/HuffmanTreeCode.cs
new file mode 100644
index 0000000000..fe582b8f29
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/HuffmanTreeCode.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ ///
+ /// Represents the tree codes (depth and bits array).
+ ///
+ internal class HuffmanTreeCode
+ {
+ ///
+ /// Gets the number of symbols.
+ ///
+ public int NumSymbols { get; }
+
+ ///
+ /// Gets the code lengths of the symbols.
+ ///
+ public byte[] CodeLengths { get; }
+
+ ///
+ /// Gets the symbol Codes.
+ ///
+ public short[] Codes { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/HuffmanTreeToken.cs b/src/ImageSharp/Formats/WebP/Lossless/HuffmanTreeToken.cs
new file mode 100644
index 0000000000..a140a2c21b
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/HuffmanTreeToken.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ ///
+ /// Holds the tree header in coded form.
+ ///
+ internal class HuffmanTreeToken
+ {
+ ///
+ /// Gets the code. Value (0..15) or escape code (16, 17, 18).
+ ///
+ public byte Code { get; }
+
+ ///
+ /// Gets extra bits for escape codes.
+ ///
+ public byte ExtraBits { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/WebP/Lossless/PixOrCopy.cs
new file mode 100644
index 0000000000..30ee2f5f47
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/PixOrCopy.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal class PixOrCopy
+ {
+ public PixOrCopyMode Mode { get; }
+
+ public short Len { get; }
+
+ public uint ArgbOrDistance { get; }
+
+ public bool IsLiteral()
+ {
+ return this.Mode == PixOrCopyMode.Literal;
+ }
+
+ public bool IsCacheIdx()
+ {
+ return this.Mode == PixOrCopyMode.CacheIdx;
+ }
+
+ public bool IsCopy()
+ {
+ return this.Mode == PixOrCopyMode.Copy;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/PixOrCopyMode.cs b/src/ImageSharp/Formats/WebP/Lossless/PixOrCopyMode.cs
new file mode 100644
index 0000000000..043a174fca
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/PixOrCopyMode.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal enum PixOrCopyMode
+ {
+ Literal,
+
+ CacheIdx,
+
+ Copy,
+
+ None
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LBackwardRefs.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LBackwardRefs.cs
new file mode 100644
index 0000000000..b5da33ea30
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LBackwardRefs.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal class Vp8LBackwardRefs
+ {
+ ///
+ /// Common block-size.
+ ///
+ public int BlockSize { get; set; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
index 9e35cc1cc8..7df49bac3e 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
@@ -12,9 +12,31 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
internal class Vp8LEncoder : IDisposable
{
- public Vp8LEncoder(MemoryAllocator memoryAllocator)
+ ///
+ /// Maximum number of reference blocks the image will be segmented into.
+ ///
+ private const int MaxRefsBlockPerImage = 16;
+
+ ///
+ /// Minimum block size for backward references.
+ ///
+ private const int MinBlockSize = 256;
+
+ public Vp8LEncoder(MemoryAllocator memoryAllocator, int width, int height)
{
+ var pixelCount = width * height;
+
this.Palette = memoryAllocator.Allocate(WebPConstants.MaxPaletteSize);
+ this.Refs = new Vp8LBackwardRefs[3];
+ this.HashChain = new Vp8LHashChain(pixelCount);
+
+ // We round the block size up, so we're guaranteed to have at most MAX_REFS_BLOCK_PER_IMAGE blocks used:
+ int refsBlockSize = ((pixelCount - 1) / MaxRefsBlockPerImage) + 1;
+ for (int i = 0; i < this.Refs.Length; ++i)
+ {
+ this.Refs[i] = new Vp8LBackwardRefs();
+ this.Refs[i].BlockSize = (refsBlockSize < MinBlockSize) ? MinBlockSize : refsBlockSize;
+ }
}
///
@@ -28,24 +50,24 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
public int TransformBits { get; set; }
///
- /// Gets or sets the cache bits.
+ /// Gets or sets a value indicating whether to use a color cache.
///
- public bool CacheBits { get; }
+ public bool UseColorCache { get; set; }
///
- /// Gets a value indicating whether to use the cross color transform.
+ /// Gets or sets a value indicating whether to use the cross color transform.
///
- public bool UseCrossColorTransform { get; }
+ public bool UseCrossColorTransform { get; set; }
///
- /// Gets a value indicating whether to use the substract green transform.
+ /// Gets or sets a value indicating whether to use the substract green transform.
///
- public bool UseSubtractGreenTransform { get; }
+ public bool UseSubtractGreenTransform { get; set; }
///
- /// Gets a value indicating whether to use the predictor transform.
+ /// Gets or sets a value indicating whether to use the predictor transform.
///
- public bool UsePredictorTransform { get; }
+ public bool UsePredictorTransform { get; set; }
///
/// Gets or sets a value indicating whether to use color indexing transform.
@@ -62,6 +84,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
public IMemoryOwner Palette { get; }
+ public Vp8LBackwardRefs[] Refs { get; }
+
+ public Vp8LHashChain HashChain { get; }
+
///
public void Dispose()
{
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LHashChain.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHashChain.cs
new file mode 100644
index 0000000000..0639b545ab
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHashChain.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+using System;
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal class Vp8LHashChain
+ {
+ ///
+ /// The 20 most significant bits contain the offset at which the best match is found.
+ /// These 20 bits are the limit defined by GetWindowSizeForHashChain (through WindowSize = 1 << 20).
+ /// The lower 12 bits contain the length of the match. The 12 bit limit is
+ /// defined in MaxFindCopyLength with MAX_LENGTH=4096.
+ ///
+ public uint[] OffsetLength { get; }
+
+ ///
+ /// This is the maximum size of the hash_chain that can be constructed.
+ /// Typically this is the pixel count (width x height) for a given image.
+ ///
+ public int Size { get; }
+
+ public Vp8LHashChain(int size)
+ {
+ this.OffsetLength = new uint[size];
+ this.OffsetLength.AsSpan().Fill(0xcdcdcdcd);
+ this.Size = size;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs
new file mode 100644
index 0000000000..af0a575267
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal class Vp8LHistogram
+ {
+ public double EstimateBits()
+ {
+ // TODO: implement this.
+ return 0.0;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LLz77Type.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LLz77Type.cs
new file mode 100644
index 0000000000..63d9f6e024
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LLz77Type.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal enum Vp8LLz77Type
+ {
+ Lz77Standard = 1,
+
+ Lz77Rle = 2,
+
+ Lz77Box = 4
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LRefsCursor.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LRefsCursor.cs
new file mode 100644
index 0000000000..ce423c39a5
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LRefsCursor.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal class Vp8LRefsCursor
+ {
+ public Vp8LRefsCursor(Vp8LBackwardRefs[] refs)
+ {
+ //this.Refs = refs;
+ this.CurrentPos = 0;
+ }
+
+ public PixOrCopy[] Refs { get; }
+
+ public int CurrentPos { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs
index 8437a091b6..97d5a57c9a 100644
--- a/src/ImageSharp/Formats/WebP/WebPConstants.cs
+++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs
@@ -102,6 +102,11 @@ namespace SixLabors.ImageSharp.Formats.WebP
///
public const int MaxNumberOfTransforms = 4;
+ ///
+ /// The bit to be written when next data to be read is a transform.
+ ///
+ public const int TransformPresent = 1;
+
///
/// The maximum allowed width or height of a webp image.
///
@@ -123,6 +128,8 @@ namespace SixLabors.ImageSharp.Formats.WebP
public const int NumDistanceCodes = 40;
+ public const int CodeLengthCodes = 19;
+
public const int LengthTableBits = 7;
public const uint CodeLengthLiterals = 16;
diff --git a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
index f9d2d7b89c..5a5de99093 100644
--- a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
+++ b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
@@ -106,7 +106,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
private void EncodeStream(Image image)
where TPixel : unmanaged, IPixel
{
- var encoder = new Vp8LEncoder(this.memoryAllocator);
+ var encoder = new Vp8LEncoder(this.memoryAllocator, image.Width, image.Height);
// Analyze image (entropy, num_palettes etc).
this.EncoderAnalyze(image, encoder);
@@ -118,17 +118,134 @@ namespace SixLabors.ImageSharp.Formats.WebP
private void EncoderAnalyze(Image image, Vp8LEncoder enc)
where TPixel : unmanaged, IPixel
{
+ int width = image.Width;
+ int height = image.Height;
+
// Check if we only deal with a small number of colors and should use a palette.
var usePalette = this.AnalyzeAndCreatePalette(image, enc);
// Empirical bit sizes.
int method = 4; // TODO: method hardcoded to 4 for now.
- enc.HistoBits = GetHistoBits(method, usePalette, image.Width, image.Height);
+ enc.HistoBits = GetHistoBits(method, usePalette, width, height);
enc.TransformBits = GetTransformBits(method, enc.HistoBits);
+ // Convert image pixels to bgra array.
+ using System.Buffers.IMemoryOwner bgraBuffer = this.memoryAllocator.Allocate(width * height);
+ Span bgra = bgraBuffer.Memory.Span;
+ int idx = 0;
+ for (int y = 0; y < height; y++)
+ {
+ Span rowSpan = image.GetPixelRowSpan(y);
+ for (int x = 0; x < rowSpan.Length; x++)
+ {
+ bgra[idx++] = ToBgra32(rowSpan[x]).PackedValue;
+ }
+ }
+
// Try out multiple LZ77 on images with few colors.
var nlz77s = (enc.PaletteSize > 0 && enc.PaletteSize <= 16) ? 2 : 1;
- this.AnalyzeEntropy(image, usePalette, enc.PaletteSize, enc.TransformBits, out bool redAndBlueAlwaysZero);
+ EntropyIx entropyIdx = this.AnalyzeEntropy(image, usePalette, enc.PaletteSize, enc.TransformBits, out bool redAndBlueAlwaysZero);
+
+ enc.UsePalette = entropyIdx == EntropyIx.Palette;
+ enc.UseSubtractGreenTransform = (entropyIdx == EntropyIx.SubGreen) || (entropyIdx == EntropyIx.SpatialSubGreen);
+ enc.UsePredictorTransform = (entropyIdx == EntropyIx.Spatial) || (entropyIdx == EntropyIx.SpatialSubGreen);
+ enc.UseCrossColorTransform = redAndBlueAlwaysZero ? false : enc.UsePredictorTransform;
+ enc.UseColorCache = false;
+
+ // Encode palette.
+ if (enc.UsePalette)
+ {
+ this.EncodePalette(image, bgra, enc);
+ }
+ }
+
+ ///
+ /// Save the palette to the bitstream.
+ ///
+ /// The image.
+ /// The Vp8L Encoder.
+ private void EncodePalette(Image image, Span bgra, Vp8LEncoder enc)
+ where TPixel : unmanaged, IPixel
+ {
+ var tmpPalette = new uint[WebPConstants.MaxPaletteSize];
+ int paletteSize = enc.PaletteSize;
+ Span palette = enc.Palette.Memory.Span;
+ this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
+ this.bitWriter.PutBits((uint)Vp8LTransformType.ColorIndexingTransform, 2);
+ this.bitWriter.PutBits((uint)paletteSize - 1, 8);
+ for (int i = paletteSize - 1; i >= 1; i--)
+ {
+ tmpPalette[i] = LosslessUtils.SubPixels(palette[i], palette[i - 1]);
+ }
+
+ tmpPalette[0] = palette[0];
+ this.EncodeImageNoHuffman(image, tmpPalette, enc);
+ }
+
+ private void EncodeImageNoHuffman(Image image, Span bgra, Vp8LEncoder enc)
+ where TPixel : unmanaged, IPixel
+ {
+ int width = image.Width;
+ int height = image.Height;
+ int paletteSize = enc.PaletteSize;
+ Vp8LHashChain hashChain = enc.HashChain;
+ var huffmanCodes = new HuffmanTreeCode[5];
+ HuffmanTreeToken[] tokens;
+ var huffTree = new HuffmanTree[3UL * WebPConstants.CodeLengthCodes];
+
+ int quality = 20; // TODO: hardcoded for now.
+
+ // Calculate backward references from ARGB image.
+ BackwardReferenceEncoder.HashChainFill(hashChain, bgra, quality, paletteSize, 1);
+
+ //var refs = GetBackwardReferences(width, height, argb, quality, 0, kLZ77Standard | kLZ77RLE, cacheBits, hashChain, refsTmp1, refsTmp2);
+
+ // Build histogram image and symbols from backward references.
+ //VP8LHistogramStoreRefs(refs, histogram_image->histograms[0]);
+
+ // Create Huffman bit lengths and codes for each histogram image.
+ //GetHuffBitLengthsAndCodes(histogram_image, huffman_codes)
+
+ // No color cache, no Huffman image.
+ this.bitWriter.PutBits(0, 1);
+
+ // Find maximum number of symbols for the huffman tree-set.
+ /*for (i = 0; i < 5; ++i)
+ {
+ HuffmanTreeCode * const codes = &huffman_codes[i];
+ if (max_tokens < codes->num_symbols)
+ {
+ max_tokens = codes->num_symbols;
+ }
+ }*/
+
+ // Store Huffman codes.
+ /*
+ for (i = 0; i < 5; ++i)
+ {
+ HuffmanTreeCode * const codes = &huffman_codes[i];
+ StoreHuffmanCode(bw, huff_tree, tokens, codes);
+ ClearHuffmanTreeIfOnlyOneSymbol(codes);
+ }
+
+ // Store actual literals.
+ StoreImageToBitMask(bw, width, 0, refs, histogram_symbols, huffman_codes);
+ */
+ }
+
+ private void StoreImageToBitMask(int width, int histoBits, short[] histogramSymbols, HuffmanTreeCode[] huffmanCodes)
+ {
+ int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1;
+ int tileMask = (histoBits == 0) ? 0 : -(1 << histoBits);
+
+ // x and y trace the position in the image.
+ int x = 0;
+ int y = 0;
+ int tileX = x & tileMask;
+ int tileY = y & tileMask;
+ int histogramIx = histogramSymbols[0];
+ Span codes = huffmanCodes.AsSpan(5 * histogramIx);
+
}
///
@@ -161,10 +278,10 @@ namespace SixLabors.ImageSharp.Formats.WebP
Span prevRow = null;
for (int y = 0; y < height; y++)
{
+ Span currentRow = image.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
- Span currentRow = image.GetPixelRowSpan(y);
- Bgra32 pix = ToBgra32(currentRow[0]);
+ Bgra32 pix = ToBgra32(currentRow[x]);
uint pixDiff = LosslessUtils.SubPixels(pix.PackedValue, pixPrev.PackedValue);
pixPrev = pix;
if ((pixDiff == 0) || (prevRow != null && pix == ToBgra32(prevRow[x])))
@@ -180,25 +297,26 @@ namespace SixLabors.ImageSharp.Formats.WebP
histo.Slice((int)HistoIx.HistoBlue * 256));
AddSingle(
pixDiff,
- histo.Slice((int)HistoIx.HistoAlpha * 256),
- histo.Slice((int)HistoIx.HistoRed * 256),
- histo.Slice((int)HistoIx.HistoGreen * 256),
- histo.Slice((int)HistoIx.HistoBlue * 256));
+ histo.Slice((int)HistoIx.HistoAlphaPred * 256),
+ histo.Slice((int)HistoIx.HistoRedPred * 256),
+ histo.Slice((int)HistoIx.HistoGreenPred * 256),
+ histo.Slice((int)HistoIx.HistoBluePred * 256));
AddSingleSubGreen(
pix.PackedValue,
histo.Slice((int)HistoIx.HistoRedSubGreen * 256),
histo.Slice((int)HistoIx.HistoBlueSubGreen * 256));
AddSingleSubGreen(
pixDiff,
- histo.Slice((int)HistoIx.HistoRedSubGreen * 256),
- histo.Slice((int)HistoIx.HistoBlueSubGreen * 256));
+ histo.Slice((int)HistoIx.HistoRedPredSubGreen * 256),
+ histo.Slice((int)HistoIx.HistoBluePredSubGreen * 256));
// Approximate the palette by the entropy of the multiplicative hash.
uint hash = HashPix(pix.PackedValue);
histo[((int)HistoIx.HistoPalette * 256) + (int)hash]++;
-
- prevRow = currentRow;
}
+
+ var histo0 = histo[0];
+ prevRow = currentRow;
}
var entropyComp = new double[(int)HistoIx.HistoTotal];
@@ -206,7 +324,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
int lastModeToAnalyze = usePalette ? (int)EntropyIx.Palette : (int)EntropyIx.SpatialSubGreen;
// Let's add one zero to the predicted histograms. The zeros are removed
- // too efficiently by the pix_diff == 0 comparison, at least one of the
+ // too efficiently by the pixDiff == 0 comparison, at least one of the
// zeros is likely to exist.
histo[(int)HistoIx.HistoRedPredSubGreen * 256]++;
histo[(int)HistoIx.HistoBluePredSubGreen * 256]++;
@@ -218,8 +336,9 @@ namespace SixLabors.ImageSharp.Formats.WebP
for (int j = 0; j < (int)HistoIx.HistoTotal; ++j)
{
var bitEntropy = new Vp8LBitEntropy();
- bitEntropy.BitsEntropyUnrefined(histo, 256);
- entropyComp[j] = bitEntropy.BitsEntropyRefine(histo.Slice(j * 256), 256);
+ Span curHisto = histo.Slice(j * 256, 256);
+ bitEntropy.BitsEntropyUnrefined(curHisto, 256);
+ entropyComp[j] = bitEntropy.BitsEntropyRefine(curHisto, 256);
}
entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] +
@@ -247,8 +366,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
LosslessUtils.SubSampleSize(height, transformBits) *
LosslessUtils.FastLog2(14);
- // For color transforms: 24 as only 3 channels are considered in a
- // ColorTransformElement.
+ // For color transforms: 24 as only 3 channels are considered in a ColorTransformElement.
entropy[(int)EntropyIx.SpatialSubGreen] += LosslessUtils.SubSampleSize(width, transformBits) *
LosslessUtils.SubSampleSize(height, transformBits) *
LosslessUtils.FastLog2(24);
@@ -260,7 +378,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
entropy[(int)EntropyIx.Palette] += paletteSize * 8;
EntropyIx minEntropyIx = EntropyIx.Direct;
- for (int k = (int)EntropyIx.Direct + 1; k <= lastModeToAnalyze; ++k)
+ for (int k = (int)EntropyIx.Direct + 1; k <= lastModeToAnalyze; k++)
{
if (entropy[(int)minEntropyIx] > entropy[k])
{
@@ -307,6 +425,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
enc.PaletteSize = this.GetColorPalette(image, palette);
if (enc.PaletteSize > WebPConstants.MaxPaletteSize)
{
+ enc.PaletteSize = 0;
return false;
}
diff --git a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs
index fc1ec3258f..8b1466c235 100644
--- a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs
+++ b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs
@@ -236,6 +236,17 @@ namespace SixLabors.ImageSharp.Formats.WebP
0x40, 0x72, 0x7e, 0x61, 0x6f, 0x50, 0x71, 0x7f, 0x60, 0x70
};
+ public static readonly uint[] PlaneToCodeLut = {
+ 96, 73, 55, 39, 23, 13, 5, 1, 255, 255, 255, 255, 255, 255, 255, 255,
+ 101, 78, 58, 42, 26, 16, 8, 2, 0, 3, 9, 17, 27, 43, 59, 79,
+ 102, 86, 62, 46, 32, 20, 10, 6, 4, 7, 11, 21, 33, 47, 63, 87,
+ 105, 90, 70, 52, 37, 28, 18, 14, 12, 15, 19, 29, 38, 53, 71, 91,
+ 110, 99, 82, 66, 48, 35, 30, 24, 22, 25, 31, 36, 49, 67, 83, 100,
+ 115, 108, 94, 76, 64, 50, 44, 40, 34, 41, 45, 51, 65, 77, 95, 109,
+ 118, 113, 103, 92, 80, 68, 60, 56, 54, 57, 61, 69, 81, 93, 104, 114,
+ 119, 116, 111, 106, 97, 88, 84, 74, 72, 75, 85, 89, 98, 107, 112, 117
+ };
+
// 31 ^ clz(i)
public static readonly byte[] LogTable8bit =
{