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 = {