diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
index ab7d03c7a6..9ff4576485 100644
--- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
+++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
@@ -1,5 +1,5 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the GNU Affero General Public License, Version 3.
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
diff --git a/src/ImageSharp/Formats/WebP/Lossless/DominantCostRange.cs b/src/ImageSharp/Formats/WebP/Lossless/DominantCostRange.cs
index f81cc2c9f4..7ce42b5bb1 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/DominantCostRange.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/DominantCostRange.cs
@@ -1,5 +1,5 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the GNU Affero General Public License, Version 3.
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.WebP.Lossless
{
diff --git a/src/ImageSharp/Formats/WebP/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/HistogramEncoder.cs
index 6e1b6ab70b..064152c02c 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/HistogramEncoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/HistogramEncoder.cs
@@ -1,5 +1,5 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the GNU Affero General Public License, Version 3.
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
index 32cc7d7712..aac3bd52ed 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
@@ -1,9 +1,14 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the GNU Affero General Public License, Version 3.
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.WebP.BitWriter;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.WebP.Lossless
{
@@ -27,6 +32,17 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
private MemoryAllocator memoryAllocator;
+ ///
+ /// A bit writer for writing lossless webp streams.
+ ///
+ private Vp8LBitWriter bitWriter;
+
+ private const int ApplyPaletteGreedyMax = 4;
+
+ private const int PaletteInvSizeBits = 11;
+
+ private const int PaletteInvSize = 1 << PaletteInvSizeBits;
+
///
/// Initializes a new instance of the class.
///
@@ -36,7 +52,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
public Vp8LEncoder(MemoryAllocator memoryAllocator, int width, int height)
{
var pixelCount = width * height;
+ int initialSize = pixelCount * 2;
+ this.bitWriter = new Vp8LBitWriter(initialSize);
this.Bgra = memoryAllocator.Allocate(pixelCount);
this.Palette = memoryAllocator.Allocate(WebPConstants.MaxPaletteSize);
this.Refs = new Vp8LBackwardRefs[3];
@@ -47,8 +65,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
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;
+ this.Refs[i] = new Vp8LBackwardRefs
+ {
+ BlockSize = (refsBlockSize < MinBlockSize) ? MinBlockSize : refsBlockSize
+ };
}
}
@@ -58,7 +78,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
public IMemoryOwner Bgra { get; }
///
- /// Gets the scratch memory for bgra rows used for prediction.
+ /// Gets or sets the scratch memory for bgra rows used for prediction.
///
public IMemoryOwner BgraScratch { get; set; }
@@ -127,6 +147,1339 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
public Vp8LHashChain HashChain { get; }
+ public void Encode(Image image, Stream stream)
+ where TPixel : unmanaged, IPixel
+ {
+ // Write the image size.
+ int width = image.Width;
+ int height = image.Height;
+ this.WriteImageSize(width, height);
+
+ // Write the non-trivial Alpha flag and lossless version.
+ bool hasAlpha = false; // TODO: for the start, this will be always false.
+ this.WriteAlphaAndVersion(hasAlpha);
+
+ // Encode the main image stream.
+ this.EncodeStream(image);
+
+ // TODO: write bytes from the bitwriter to the stream.
+ }
+
+ ///
+ /// Writes the image size to the stream.
+ ///
+ /// The input image width.
+ /// The input image height.
+ private void WriteImageSize(int inputImgWidth, int inputImgHeight)
+ {
+ Guard.MustBeLessThan(inputImgWidth, WebPConstants.MaxDimension, nameof(inputImgWidth));
+ Guard.MustBeLessThan(inputImgHeight, WebPConstants.MaxDimension, nameof(inputImgHeight));
+
+ uint width = (uint)inputImgWidth - 1;
+ uint height = (uint)inputImgHeight - 1;
+
+ this.bitWriter.PutBits(width, WebPConstants.Vp8LImageSizeBits);
+ this.bitWriter.PutBits(height, WebPConstants.Vp8LImageSizeBits);
+ }
+
+ private void WriteAlphaAndVersion(bool hasAlpha)
+ {
+ this.bitWriter.PutBits(hasAlpha ? 1U : 0, 1);
+ this.bitWriter.PutBits(WebPConstants.Vp8LVersion, WebPConstants.Vp8LVersionBits);
+ }
+
+ ///
+ /// Encodes the image stream using lossless webp format.
+ ///
+ /// The pixel type.
+ /// The image to encode.
+ private void EncodeStream(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ int width = image.Width;
+ int height = image.Height;
+ int bytePosition = this.bitWriter.NumBytes();
+
+ // Convert image pixels to bgra array.
+ Span bgra = this.Bgra.GetSpan();
+ 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;
+ }
+ }
+
+ // Analyze image (entropy, numPalettes etc).
+ this.EncoderAnalyze(image);
+
+ var entropyIdx = 3; // TODO: hardcoded for now.
+ int quality = 75; // TODO: quality is hardcoded for now.
+ bool useCache = true; // TODO: useCache is hardcoded for now.
+ bool redAndBlueAlwaysZero = false;
+
+ this.UsePalette = entropyIdx == (int)EntropyIx.Palette;
+ this.UseSubtractGreenTransform = (entropyIdx == (int)EntropyIx.SubGreen) || (entropyIdx == (int)EntropyIx.SpatialSubGreen);
+ this.UsePredictorTransform = (entropyIdx == (int)EntropyIx.Spatial) || (entropyIdx == (int)EntropyIx.SpatialSubGreen);
+ this.UseCrossColorTransform = redAndBlueAlwaysZero ? false : this.UsePredictorTransform;
+ this.AllocateTransformBuffer(width, height);
+
+ // Reset any parameter in the encoder that is set in the previous iteration.
+ this.CacheBits = 0;
+ this.ClearRefs();
+
+ // TODO: Apply near-lossless preprocessing.
+
+ // Encode palette.
+ if (this.UsePalette)
+ {
+ this.EncodePalette();
+ this.MapImageFromPalette(width, height);
+
+ // If using a color cache, do not have it bigger than the number of colors.
+ if (useCache && this.PaletteSize < (1 << WebPConstants.MaxColorCacheBits))
+ {
+ this.CacheBits = WebPCommonUtils.BitsLog2Floor((uint)this.PaletteSize) + 1;
+ }
+ }
+
+ // Apply transforms and write transform data.
+ if (this.UseSubtractGreenTransform)
+ {
+ this.ApplySubtractGreen(this.CurrentWidth, height);
+ }
+
+ if (this.UsePredictorTransform)
+ {
+ this.ApplyPredictFilter(this.CurrentWidth, height, quality, this.UseSubtractGreenTransform);
+ }
+
+ if (this.UseCrossColorTransform)
+ {
+ this.ApplyCrossColorFilter(this.CurrentWidth, height, quality);
+ }
+
+ this.bitWriter.PutBits(0, 1); // No more transforms.
+
+ // Encode and write the transformed image.
+ this.EncodeImage(bgra, this.HashChain, this.Refs, this.CurrentWidth, height, quality, useCache, this.CacheBits, this.HistoBits, bytePosition);
+ }
+
+ ///
+ /// Analyzes the image and decides what transforms should be used.
+ ///
+ private void EncoderAnalyze(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ int method = 4; // TODO: method hardcoded to 4 for now.
+ 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);
+
+ // Empirical bit sizes.
+ this.HistoBits = GetHistoBits(method, usePalette, width, height);
+ this.TransformBits = GetTransformBits(method, this.HistoBits);
+
+ // Try out multiple LZ77 on images with few colors.
+ var nlz77s = (this.PaletteSize > 0 && this.PaletteSize <= 16) ? 2 : 1;
+ EntropyIx entropyIdx = this.AnalyzeEntropy(image, usePalette, this.PaletteSize, this.TransformBits, out bool redAndBlueAlwaysZero);
+
+ // TODO: Fill CrunchConfig
+ }
+
+ private void EncodeImage(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs[] refsArray, int width, int height, int quality, bool useCache, int cacheBits, int histogramBits, int initBytePosition)
+ {
+ int lz77sTypesToTrySize = 1; // TODO: hardcoded for now.
+ int[] lz77sTypesToTry = { 3 };
+ int histogramImageXySize = LosslessUtils.SubSampleSize(width, histogramBits) * LosslessUtils.SubSampleSize(height, histogramBits);
+ var histogramSymbols = new short[histogramImageXySize];
+ var huffTree = new HuffmanTree[3 * WebPConstants.CodeLengthCodes];
+ for (int i = 0; i < huffTree.Length; i++)
+ {
+ huffTree[i] = new HuffmanTree();
+ }
+
+ if (useCache)
+ {
+ if (cacheBits == 0)
+ {
+ cacheBits = WebPConstants.MaxColorCacheBits;
+ }
+ }
+ else
+ {
+ cacheBits = 0;
+ }
+
+ // Calculate backward references from ARGB image.
+ BackwardReferenceEncoder.HashChainFill(hashChain, bgra, quality, width, height);
+
+ // TODO: BitWriterInit(&bw_best, 0)
+ // BitWriterClone(bw, &bw_best))
+
+ for (int lz77sIdx = 0; lz77sIdx < lz77sTypesToTrySize; lz77sIdx++)
+ {
+ Vp8LBackwardRefs refsBest = BackwardReferenceEncoder.GetBackwardReferences(width, height, bgra, quality, lz77sTypesToTry[lz77sIdx], ref cacheBits, hashChain, refsArray[0], refsArray[1]);
+
+ // Keep the best references aside and use the other element from the first
+ // two as a temporary for later usage.
+ Vp8LBackwardRefs refsTmp = refsArray[refsBest.Equals(refsArray[0]) ? 1 : 0];
+
+ var tmpHisto = new Vp8LHistogram(cacheBits);
+ var histogramImage = new List(histogramImageXySize);
+ for (int i = 0; i < histogramImageXySize; i++)
+ {
+ histogramImage.Add(new Vp8LHistogram(cacheBits));
+ }
+
+ // Build histogram image and symbols from backward references.
+ HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, quality, histogramBits, cacheBits, histogramImage, tmpHisto, histogramSymbols);
+
+ // Create Huffman bit lengths and codes for each histogram image.
+ var histogramImageSize = histogramImage.Count;
+ var bitArraySize = 5 * histogramImageSize;
+ var huffmanCodes = new HuffmanTreeCode[bitArraySize];
+ for (int i = 0; i < huffmanCodes.Length; i++)
+ {
+ huffmanCodes[i] = new HuffmanTreeCode();
+ }
+
+ GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
+
+ // Color Cache parameters.
+ if (cacheBits > 0)
+ {
+ this.bitWriter.PutBits(1, 1);
+ this.bitWriter.PutBits((uint)cacheBits, 4);
+ }
+ else
+ {
+ this.bitWriter.PutBits(0, 1);
+ }
+
+ // Huffman image + meta huffman.
+ bool writeHistogramImage = histogramImageSize > 1;
+ this.bitWriter.PutBits((uint)(writeHistogramImage ? 1 : 0), 1);
+ if (writeHistogramImage)
+ {
+ using IMemoryOwner histogramArgbBuffer = this.memoryAllocator.Allocate(histogramImageXySize);
+ Span histogramArgb = histogramArgbBuffer.GetSpan();
+ int maxIndex = 0;
+ for (int i = 0; i < histogramImageXySize; i++)
+ {
+ int symbolIndex = histogramSymbols[i] & 0xffff;
+ histogramArgb[i] = (uint)(symbolIndex << 8);
+ if (symbolIndex >= maxIndex)
+ {
+ maxIndex = symbolIndex + 1;
+ }
+ }
+
+ histogramImageSize = maxIndex;
+ this.bitWriter.PutBits((uint)(histogramBits - 2), 3);
+ this.EncodeImageNoHuffman(histogramArgb, hashChain, refsTmp, refsArray[2], LosslessUtils.SubSampleSize(width, histogramBits), LosslessUtils.SubSampleSize(height, histogramBits), quality);
+ }
+
+ // Store Huffman codes.
+ // Find maximum number of symbols for the huffman tree-set.
+ int maxTokens = 0;
+ for (int i = 0; i < 5 * histogramImageSize; i++)
+ {
+ HuffmanTreeCode codes = huffmanCodes[i];
+ if (maxTokens < codes.NumSymbols)
+ {
+ maxTokens = codes.NumSymbols;
+ }
+ }
+
+ var tokens = new HuffmanTreeToken[maxTokens];
+ for (int i = 0; i < tokens.Length; i++)
+ {
+ tokens[i] = new HuffmanTreeToken();
+ }
+
+ for (int i = 0; i < 5 * histogramImageSize; i++)
+ {
+ HuffmanTreeCode codes = huffmanCodes[i];
+ this.StoreHuffmanCode(huffTree, tokens, codes);
+ ClearHuffmanTreeIfOnlyOneSymbol(codes);
+ }
+
+ // Store actual literals.
+ var hdrSizeTmp = (int)(this.bitWriter.NumBytes() - initBytePosition);
+ this.StoreImageToBitMask(width, histogramBits, refsBest, histogramSymbols, huffmanCodes);
+
+ // TODO: Keep track of the smallest image so far.
+ }
+ }
+
+ ///
+ /// Save the palette to the bitstream.
+ ///
+ private void EncodePalette()
+ {
+ Span tmpPalette = new uint[WebPConstants.MaxPaletteSize];
+ int paletteSize = this.PaletteSize;
+ Span palette = this.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(tmpPalette, this.HashChain, this.Refs[0], this.Refs[1], width: paletteSize, height: 1, quality: 20);
+ }
+
+ ///
+ /// Applies the subtract green transformation to the pixel data of the image.
+ ///
+ /// The width of the image.
+ /// The height of the image.
+ private void ApplySubtractGreen(int width, int height)
+ {
+ this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
+ this.bitWriter.PutBits((uint)Vp8LTransformType.SubtractGreen, 2);
+ LosslessUtils.SubtractGreenFromBlueAndRed(this.Bgra.GetSpan(), width * height);
+ }
+
+ private void ApplyPredictFilter(int width, int height, int quality, bool usedSubtractGreen)
+ {
+ int nearLosslessStrength = 100; // TODO: for now always 100
+ bool exact = false; // TODO: always false for now.
+ int predBits = this.TransformBits;
+ int transformWidth = LosslessUtils.SubSampleSize(width, predBits);
+ int transformHeight = LosslessUtils.SubSampleSize(height, predBits);
+
+ PredictorEncoder.ResidualImage(width, height, predBits, this.Bgra.GetSpan(), this.BgraScratch.GetSpan(), this.TransformData.GetSpan(), nearLosslessStrength, exact, usedSubtractGreen);
+
+ this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
+ this.bitWriter.PutBits((uint)Vp8LTransformType.PredictorTransform, 2);
+ this.bitWriter.PutBits((uint)(predBits - 2), 3);
+
+ this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, quality);
+ }
+
+ private void ApplyCrossColorFilter(int width, int height, int quality)
+ {
+ int colorTransformBits = this.TransformBits;
+ int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits);
+ int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits);
+
+ PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, quality, this.Bgra.GetSpan(), this.TransformData.GetSpan());
+
+ this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
+ this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2);
+ this.bitWriter.PutBits((uint)(colorTransformBits - 2), 3);
+
+ this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, quality);
+ }
+
+ private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs refsTmp1, Vp8LBackwardRefs refsTmp2, int width, int height, int quality)
+ {
+ int cacheBits = 0;
+ var histogramSymbols = new short[1]; // Only one tree, one symbol.
+ var huffmanCodes = new HuffmanTreeCode[5];
+ for (int i = 0; i < huffmanCodes.Length; i++)
+ {
+ huffmanCodes[i] = new HuffmanTreeCode();
+ }
+
+ var huffTree = new HuffmanTree[3UL * WebPConstants.CodeLengthCodes];
+ for (int i = 0; i < huffTree.Length; i++)
+ {
+ huffTree[i] = new HuffmanTree();
+ }
+
+ // Calculate backward references from the image pixels.
+ BackwardReferenceEncoder.HashChainFill(hashChain, bgra, quality, width, height);
+
+ Vp8LBackwardRefs refs = BackwardReferenceEncoder.GetBackwardReferences(
+ width,
+ height,
+ bgra,
+ quality,
+ (int)Vp8LLz77Type.Lz77Standard | (int)Vp8LLz77Type.Lz77Rle,
+ ref cacheBits,
+ hashChain,
+ refsTmp1,
+ refsTmp2);
+
+ var histogramImage = new List()
+ {
+ new Vp8LHistogram(cacheBits)
+ };
+
+ // Build histogram image and symbols from backward references.
+ histogramImage[0].StoreRefs(refs);
+
+ // Create Huffman bit lengths and codes for each histogram image.
+ GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
+
+ // No color cache, no Huffman image.
+ this.bitWriter.PutBits(0, 1);
+
+ // Find maximum number of symbols for the huffman tree-set.
+ int maxTokens = 0;
+ for (int i = 0; i < 5; i++)
+ {
+ HuffmanTreeCode codes = huffmanCodes[i];
+ if (maxTokens < codes.NumSymbols)
+ {
+ maxTokens = codes.NumSymbols;
+ }
+ }
+
+ var tokens = new HuffmanTreeToken[maxTokens];
+ for (int i = 0; i < tokens.Length; i++)
+ {
+ tokens[i] = new HuffmanTreeToken();
+ }
+
+ // Store Huffman codes.
+ for (int i = 0; i < 5; i++)
+ {
+ HuffmanTreeCode codes = huffmanCodes[i];
+ this.StoreHuffmanCode(huffTree, tokens, codes);
+ ClearHuffmanTreeIfOnlyOneSymbol(codes);
+ }
+
+ // Store actual literals.
+ this.StoreImageToBitMask(width, 0, refs, histogramSymbols, huffmanCodes);
+ }
+
+ private void StoreHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode)
+ {
+ int count = 0;
+ int[] symbols = { 0, 0 };
+ int maxBits = 8;
+ int maxSymbol = 1 << maxBits;
+
+ // Check whether it's a small tree.
+ for (int i = 0; i < huffmanCode.NumSymbols && count < 3; ++i)
+ {
+ if (huffmanCode.CodeLengths[i] != 0)
+ {
+ if (count < 2)
+ {
+ symbols[count] = i;
+ }
+
+ count++;
+ }
+ }
+
+ if (count == 0)
+ {
+ // emit minimal tree for empty cases
+ // bits: small tree marker: 1, count-1: 0, large 8-bit code: 0, code: 0
+ this.bitWriter.PutBits(0x01, 4);
+ }
+ else if (count <= 2 && symbols[0] < maxSymbol && symbols[1] < maxSymbol)
+ {
+ this.bitWriter.PutBits(1, 1); // Small tree marker to encode 1 or 2 symbols.
+ this.bitWriter.PutBits((uint)(count - 1), 1);
+ if (symbols[0] <= 1)
+ {
+ this.bitWriter.PutBits(0, 1); // Code bit for small (1 bit) symbol value.
+ this.bitWriter.PutBits((uint)symbols[0], 1);
+ }
+ else
+ {
+ this.bitWriter.PutBits(1, 1);
+ this.bitWriter.PutBits((uint)symbols[0], 8);
+ }
+
+ if (count == 2)
+ {
+ this.bitWriter.PutBits((uint)symbols[1], 8);
+ }
+ }
+ else
+ {
+ this.StoreFullHuffmanCode(huffTree, tokens, huffmanCode);
+ }
+ }
+
+ private void StoreFullHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree)
+ {
+ int i;
+ var codeLengthBitDepth = new byte[WebPConstants.CodeLengthCodes];
+ var codeLengthBitDepthSymbols = new short[WebPConstants.CodeLengthCodes];
+ var huffmanCode = new HuffmanTreeCode();
+ huffmanCode.NumSymbols = WebPConstants.CodeLengthCodes;
+ huffmanCode.CodeLengths = codeLengthBitDepth;
+ huffmanCode.Codes = codeLengthBitDepthSymbols;
+
+ this.bitWriter.PutBits(0, 1);
+ var numTokens = HuffmanUtils.CreateCompressedHuffmanTree(tree, tokens);
+ var histogram = new uint[WebPConstants.CodeLengthCodes + 1];
+ var bufRle = new bool[WebPConstants.CodeLengthCodes + 1];
+ for (i = 0; i < numTokens; i++)
+ {
+ histogram[tokens[i].Code]++;
+ }
+
+ HuffmanUtils.CreateHuffmanTree(histogram, 7, bufRle, huffTree, huffmanCode);
+ this.StoreHuffmanTreeOfHuffmanTreeToBitMask(codeLengthBitDepth);
+ ClearHuffmanTreeIfOnlyOneSymbol(huffmanCode);
+
+ int trailingZeroBits = 0;
+ int trimmedLength = numTokens;
+ i = numTokens;
+ while (i-- > 0)
+ {
+ int ix = tokens[i].Code;
+ if (ix == 0 || ix == 17 || ix == 18)
+ {
+ trimmedLength--; // discount trailing zeros.
+ trailingZeroBits += codeLengthBitDepth[ix];
+ if (ix == 17)
+ {
+ trailingZeroBits += 3;
+ }
+ else if (ix == 18)
+ {
+ trailingZeroBits += 7;
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ var writeTrimmedLength = trimmedLength > 1 && trailingZeroBits > 12;
+ var length = writeTrimmedLength ? trimmedLength : numTokens;
+ this.bitWriter.PutBits((uint)(writeTrimmedLength ? 1 : 0), 1);
+ if (writeTrimmedLength)
+ {
+ if (trimmedLength == 2)
+ {
+ this.bitWriter.PutBits(0, 3 + 2); // nbitpairs=1, trimmed_length=2
+ }
+ else
+ {
+ int nbits = WebPCommonUtils.BitsLog2Floor((uint)trimmedLength - 2);
+ int nbitpairs = (nbits / 2) + 1;
+ this.bitWriter.PutBits((uint)nbitpairs - 1, 3);
+ this.bitWriter.PutBits((uint)trimmedLength - 2, nbitpairs * 2);
+ }
+ }
+
+ this.StoreHuffmanTreeToBitMask(tokens, length, huffmanCode);
+ }
+
+ private void StoreHuffmanTreeToBitMask(HuffmanTreeToken[] tokens, int numTokens, HuffmanTreeCode huffmanCode)
+ {
+ for (int i = 0; i < numTokens; i++)
+ {
+ int ix = tokens[i].Code;
+ int extraBits = tokens[i].ExtraBits;
+ this.bitWriter.PutBits((uint)huffmanCode.Codes[ix], huffmanCode.CodeLengths[ix]);
+ switch (ix)
+ {
+ case 16:
+ this.bitWriter.PutBits((uint)extraBits, 2);
+ break;
+ case 17:
+ this.bitWriter.PutBits((uint)extraBits, 3);
+ break;
+ case 18:
+ this.bitWriter.PutBits((uint)extraBits, 7);
+ break;
+ }
+ }
+ }
+
+ private void StoreHuffmanTreeOfHuffmanTreeToBitMask(byte[] codeLengthBitdepth)
+ {
+ // RFC 1951 will calm you down if you are worried about this funny sequence.
+ // This sequence is tuned from that, but more weighted for lower symbol count,
+ // and more spiking histograms.
+ byte[] storageOrder = { 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
+
+ // Throw away trailing zeros:
+ int codesToStore = WebPConstants.CodeLengthCodes;
+ for (; codesToStore > 4; codesToStore--)
+ {
+ if (codeLengthBitdepth[storageOrder[codesToStore - 1]] != 0)
+ {
+ break;
+ }
+ }
+
+ this.bitWriter.PutBits((uint)codesToStore - 4, 4);
+ for (int i = 0; i < codesToStore; i++)
+ {
+ this.bitWriter.PutBits(codeLengthBitdepth[storageOrder[i]], 3);
+ }
+ }
+
+ private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, 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);
+ using List.Enumerator c = backwardRefs.Refs.GetEnumerator();
+ while (c.MoveNext())
+ {
+ PixOrCopy v = c.Current;
+ if ((tileX != (x & tileMask)) || (tileY != (y & tileMask)))
+ {
+ tileX = x & tileMask;
+ tileY = y & tileMask;
+ histogramIx = histogramSymbols[((y >> histoBits) * histoXSize) + (x >> histoBits)];
+ codes = huffmanCodes.AsSpan(5 * histogramIx);
+ }
+
+ if (v.IsLiteral())
+ {
+ byte[] order = { 1, 2, 0, 3 };
+ for (int k = 0; k < 4; k++)
+ {
+ int code = (int)v.Literal(order[k]);
+ this.bitWriter.WriteHuffmanCode(codes[k], code);
+ }
+ }
+ else if (v.IsCacheIdx())
+ {
+ int code = (int)v.CacheIdx();
+ int literalIx = 256 + WebPConstants.NumLengthCodes + code;
+ this.bitWriter.WriteHuffmanCode(codes[0], literalIx);
+ }
+ else
+ {
+ int bits = 0;
+ int nBits = 0;
+ int distance = (int)v.Distance();
+ int code = LosslessUtils.PrefixEncode(v.Len, ref nBits, ref bits);
+ this.bitWriter.WriteHuffmanCodeWithExtraBits(codes[0], 256 + code, bits, nBits);
+
+ // Don't write the distance with the extra bits code since
+ // the distance can be up to 18 bits of extra bits, and the prefix
+ // 15 bits, totaling to 33, and our PutBits only supports up to 32 bits.
+ code = LosslessUtils.PrefixEncode(distance, ref nBits, ref bits);
+ this.bitWriter.WriteHuffmanCode(codes[4], code);
+ this.bitWriter.PutBits((uint)bits, nBits);
+ }
+
+ x += v.Length();
+ while (x >= width)
+ {
+ x -= width;
+ y++;
+ }
+ }
+ }
+
+ ///
+ /// Analyzes the entropy of the input image to determine which transforms to use during encoding the image.
+ ///
+ /// The pixel type of the image.
+ /// The image to analyze.
+ /// Indicates whether a palette should be used.
+ /// The palette size.
+ /// The transformation bits.
+ /// Indicates if red and blue are always zero.
+ /// The entropy mode to use.
+ private EntropyIx AnalyzeEntropy(Image image, bool usePalette, int paletteSize, int transformBits, out bool redAndBlueAlwaysZero)
+ where TPixel : unmanaged, IPixel
+ {
+ int width = image.Width;
+ int height = image.Height;
+
+ if (usePalette && paletteSize <= 16)
+ {
+ // In the case of small palettes, we pack 2, 4 or 8 pixels together. In
+ // practice, small palettes are better than any other transform.
+ redAndBlueAlwaysZero = true;
+ return EntropyIx.Palette;
+ }
+
+ using System.Buffers.IMemoryOwner histoBuffer = this.memoryAllocator.Allocate((int)HistoIx.HistoTotal * 256);
+ Span histo = histoBuffer.Memory.Span;
+ Bgra32 pixPrev = ToBgra32(image.GetPixelRowSpan(0)[0]); // Skip the first pixel.
+ Span prevRow = null;
+ for (int y = 0; y < height; y++)
+ {
+ Span currentRow = image.GetPixelRowSpan(y);
+ for (int x = 0; x < width; x++)
+ {
+ Bgra32 pix = ToBgra32(currentRow[x]);
+ uint pixDiff = LosslessUtils.SubPixels(pix.PackedValue, pixPrev.PackedValue);
+ pixPrev = pix;
+ if ((pixDiff == 0) || (prevRow != null && pix == ToBgra32(prevRow[x])))
+ {
+ continue;
+ }
+
+ AddSingle(
+ pix.PackedValue,
+ histo.Slice((int)HistoIx.HistoAlpha * 256),
+ histo.Slice((int)HistoIx.HistoRed * 256),
+ histo.Slice((int)HistoIx.HistoGreen * 256),
+ histo.Slice((int)HistoIx.HistoBlue * 256));
+ AddSingle(
+ pixDiff,
+ 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.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]++;
+ }
+
+ var histo0 = histo[0];
+ prevRow = currentRow;
+ }
+
+ var entropyComp = new double[(int)HistoIx.HistoTotal];
+ var entropy = new double[(int)EntropyIx.NumEntropyIx];
+ 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 pixDiff == 0 comparison, at least one of the
+ // zeros is likely to exist.
+ histo[(int)HistoIx.HistoRedPredSubGreen * 256]++;
+ histo[(int)HistoIx.HistoBluePredSubGreen * 256]++;
+ histo[(int)HistoIx.HistoRedPred * 256]++;
+ histo[(int)HistoIx.HistoGreenPred * 256]++;
+ histo[(int)HistoIx.HistoBluePred * 256]++;
+ histo[(int)HistoIx.HistoAlphaPred * 256]++;
+
+ for (int j = 0; j < (int)HistoIx.HistoTotal; ++j)
+ {
+ var bitEntropy = new Vp8LBitEntropy();
+ Span curHisto = histo.Slice(j * 256, 256);
+ bitEntropy.BitsEntropyUnrefined(curHisto, 256);
+ entropyComp[j] = bitEntropy.BitsEntropyRefine();
+ }
+
+ entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] +
+ entropyComp[(int)HistoIx.HistoRed] +
+ entropyComp[(int)HistoIx.HistoGreen] +
+ entropyComp[(int)HistoIx.HistoBlue];
+ entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] +
+ entropyComp[(int)HistoIx.HistoRedPred] +
+ entropyComp[(int)HistoIx.HistoGreenPred] +
+ entropyComp[(int)HistoIx.HistoBluePred];
+ entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] +
+ entropyComp[(int)HistoIx.HistoRedSubGreen] +
+ entropyComp[(int)HistoIx.HistoGreen] +
+ entropyComp[(int)HistoIx.HistoBlueSubGreen];
+ entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] +
+ entropyComp[(int)HistoIx.HistoRedPredSubGreen] +
+ entropyComp[(int)HistoIx.HistoGreenPred] +
+ entropyComp[(int)HistoIx.HistoBluePredSubGreen];
+ entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette];
+
+ // When including transforms, there is an overhead in bits from
+ // storing them. This overhead is small but matters for small images.
+ // For spatial, there are 14 transformations.
+ entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) *
+ LosslessUtils.SubSampleSize(height, transformBits) *
+ LosslessUtils.FastLog2(14);
+
+ // 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);
+
+ // For palettes, add the cost of storing the palette.
+ // We empirically estimate the cost of a compressed entry as 8 bits.
+ // The palette is differential-coded when compressed hence a much
+ // lower cost than sizeof(uint32_t)*8.
+ entropy[(int)EntropyIx.Palette] += paletteSize * 8;
+
+ EntropyIx minEntropyIx = EntropyIx.Direct;
+ for (int k = (int)EntropyIx.Direct + 1; k <= lastModeToAnalyze; k++)
+ {
+ if (entropy[(int)minEntropyIx] > entropy[k])
+ {
+ minEntropyIx = (EntropyIx)k;
+ }
+ }
+
+ redAndBlueAlwaysZero = true;
+
+ // Let's check if the histogram of the chosen entropy mode has
+ // non-zero red and blue values. If all are zero, we can later skip
+ // the cross color optimization.
+ var histoPairs = new byte[][]
+ {
+ new byte[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue },
+ new byte[] { (byte)HistoIx.HistoRedPred, (byte)HistoIx.HistoBluePred },
+ new byte[] { (byte)HistoIx.HistoRedSubGreen, (byte)HistoIx.HistoBlueSubGreen },
+ new byte[] { (byte)HistoIx.HistoRedPredSubGreen, (byte)HistoIx.HistoBluePredSubGreen },
+ new byte[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue }
+ };
+ Span redHisto = histo.Slice(256 * histoPairs[(int)minEntropyIx][0]);
+ Span blueHisto = histo.Slice(256 * histoPairs[(int)minEntropyIx][1]);
+ for (int i = 1; i < 256; i++)
+ {
+ if ((redHisto[i] | blueHisto[i]) != 0)
+ {
+ redAndBlueAlwaysZero = false;
+ break;
+ }
+ }
+
+ return minEntropyIx;
+ }
+
+ ///
+ /// If number of colors in the image is less than or equal to MAX_PALETTE_SIZE,
+ /// creates a palette and returns true, else returns false.
+ ///
+ /// true, if a palette should be used.
+ private bool AnalyzeAndCreatePalette(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ Span palette = this.Palette.Memory.Span;
+ this.PaletteSize = this.GetColorPalette(image, palette);
+ if (this.PaletteSize > WebPConstants.MaxPaletteSize)
+ {
+ this.PaletteSize = 0;
+ return false;
+ }
+
+ uint[] paletteArray = palette.Slice(0, this.PaletteSize).ToArray();
+ Array.Sort(paletteArray);
+ paletteArray.CopyTo(palette);
+
+ if (PaletteHasNonMonotonousDeltas(palette, this.PaletteSize))
+ {
+ GreedyMinimizeDeltas(palette, this.PaletteSize);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Gets the color palette.
+ ///
+ /// The pixel type of the image.
+ /// The image to get the palette from.
+ /// The span to store the palette into.
+ /// The number of palette entries.
+ private int GetColorPalette(Image image, Span palette)
+ where TPixel : unmanaged, IPixel
+ {
+ var colors = new HashSet();
+ for (int y = 0; y < image.Height; y++)
+ {
+ Span rowSpan = image.GetPixelRowSpan(y);
+ for (int x = 0; x < rowSpan.Length; x++)
+ {
+ colors.Add(rowSpan[x]);
+ if (colors.Count > WebPConstants.MaxPaletteSize)
+ {
+ // Exact count is not needed, because a palette will not be used then anyway.
+ return WebPConstants.MaxPaletteSize + 1;
+ }
+ }
+ }
+
+ // Fill the colors into the palette.
+ using HashSet.Enumerator colorEnumerator = colors.GetEnumerator();
+ int idx = 0;
+ while (colorEnumerator.MoveNext())
+ {
+ Bgra32 bgra = ToBgra32(colorEnumerator.Current);
+ palette[idx++] = bgra.PackedValue;
+ }
+
+ return colors.Count;
+ }
+
+ private void MapImageFromPalette(int width, int height)
+ {
+ Span src = this.Bgra.GetSpan();
+ int srcStride = this.CurrentWidth;
+ Span dst = this.Bgra.GetSpan(); // Applying the palette will be done in place.
+ Span palette = this.Palette.GetSpan();
+ int paletteSize = this.PaletteSize;
+ int xBits;
+
+ // Replace each input pixel by corresponding palette index.
+ // This is done line by line.
+ if (paletteSize <= 4)
+ {
+ xBits = (paletteSize <= 2) ? 3 : 2;
+ }
+ else
+ {
+ xBits = (paletteSize <= 16) ? 1 : 0;
+ }
+
+ this.ApplyPalette(src, srcStride, dst, this.CurrentWidth, palette, paletteSize, width, height, xBits);
+ }
+
+ ///
+ /// Remap argb values in src[] to packed palettes entries in dst[]
+ /// using 'row' as a temporary buffer of size 'width'.
+ /// We assume that all src[] values have a corresponding entry in the palette.
+ /// Note: src[] can be the same as dst[]
+ ///
+ private void ApplyPalette(Span src, int srcStride, Span dst, int dstStride, Span palette, int paletteSize, int width, int height, int xBits)
+ {
+ using System.Buffers.IMemoryOwner tmpRowBuffer = this.memoryAllocator.Allocate(width);
+ Span tmpRow = tmpRowBuffer.GetSpan();
+
+ if (paletteSize < ApplyPaletteGreedyMax)
+ {
+ // TODO: APPLY_PALETTE_FOR(SearchColorGreedy(palette, palette_size, pix));
+ }
+ else
+ {
+ uint[] buffer = new uint[PaletteInvSize];
+
+ // Try to find a perfect hash function able to go from a color to an index
+ // within 1 << PaletteInvSize in order to build a hash map to go from color to index in palette.
+ int i;
+ for (i = 0; i < 3; i++)
+ {
+ bool useLUT = true;
+
+ // Set each element in buffer to max value.
+ buffer.AsSpan().Fill(uint.MaxValue);
+
+ for (int j = 0; j < paletteSize; j++)
+ {
+ uint ind = 0;
+ switch (i)
+ {
+ case 0:
+ ind = ApplyPaletteHash0(palette[j]);
+ break;
+ case 1:
+ ind = ApplyPaletteHash1(palette[j]);
+ break;
+ case 2:
+ ind = ApplyPaletteHash2(palette[j]);
+ break;
+ }
+
+ if (buffer[ind] != uint.MaxValue)
+ {
+ useLUT = false;
+ break;
+ }
+ else
+ {
+ buffer[ind] = (uint)j;
+ }
+ }
+
+ if (useLUT)
+ {
+ break;
+ }
+ }
+
+ if (i == 0 || i == 1 || i == 2)
+ {
+ ApplyPaletteFor(width, height, palette, i, src, srcStride, dst, dstStride, tmpRow, buffer, xBits);
+ }
+ else
+ {
+ uint[] idxMap = new uint[paletteSize];
+ uint[] paletteSorted = new uint[paletteSize];
+ PrepareMapToPalette(palette, paletteSize, paletteSorted, idxMap);
+ ApplyPaletteForWithIdxMap(width, height, palette, src, srcStride, dst, dstStride, tmpRow, idxMap, xBits, paletteSorted, paletteSize);
+ }
+ }
+ }
+
+ private static void ApplyPaletteFor(int width, int height, Span palette, int hashIdx, Span src, int srcStride, Span dst, int dstStride, Span tmpRow, uint[] buffer, int xBits)
+ {
+ uint prevPix = palette[0];
+ uint prevIdx = 0;
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ uint pix = src[x];
+ if (pix != prevPix)
+ {
+ switch (hashIdx)
+ {
+ case 0:
+ prevIdx = buffer[ApplyPaletteHash0(pix)];
+ break;
+ case 1:
+ prevIdx = buffer[ApplyPaletteHash1(pix)];
+ break;
+ case 2:
+ prevIdx = buffer[ApplyPaletteHash2(pix)];
+ break;
+ }
+
+ prevPix = pix;
+ }
+
+ tmpRow[x] = (byte)prevIdx;
+ }
+
+ LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst);
+
+ src = src.Slice((int)srcStride);
+ dst = dst.Slice((int)dstStride);
+ }
+ }
+
+ private static void ApplyPaletteForWithIdxMap(int width, int height, Span palette, Span src, int srcStride, Span dst, int dstStride, Span tmpRow, uint[] idxMap, int xBits, uint[] paletteSorted, int paletteSize)
+ {
+ uint prevPix = palette[0];
+ uint prevIdx = 0;
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ uint pix = src[x];
+ if (pix != prevPix)
+ {
+ prevIdx = idxMap[SearchColorNoIdx(paletteSorted, pix, paletteSize)];
+ prevPix = pix;
+ }
+
+ tmpRow[x] = (byte)prevIdx;
+ }
+
+ LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst);
+
+ src = src.Slice((int)srcStride);
+ dst = dst.Slice((int)dstStride);
+ }
+ }
+
+ ///
+ /// Sort palette in increasing order and prepare an inverse mapping array.
+ ///
+ private static void PrepareMapToPalette(Span palette, int numColors, uint[] sorted, uint[] idxMap)
+ {
+ palette.Slice(numColors).CopyTo(sorted);
+ Array.Sort(sorted, PaletteCompareColorsForSort);
+ for (int i = 0; i < numColors; i++)
+ {
+ idxMap[SearchColorNoIdx(sorted, palette[i], numColors)] = (uint)i;
+ }
+ }
+
+ private static int SearchColorNoIdx(uint[] sorted, uint color, int hi)
+ {
+ int low = 0;
+ if (sorted[low] == color)
+ {
+ return low; // loop invariant: sorted[low] != color
+ }
+
+ while (true)
+ {
+ int mid = (low + hi) >> 1;
+ if (sorted[mid] == color)
+ {
+ return mid;
+ }
+ else if (sorted[mid] < color)
+ {
+ low = mid;
+ }
+ else
+ {
+ hi = mid;
+ }
+ }
+ }
+
+ private static void ClearHuffmanTreeIfOnlyOneSymbol(HuffmanTreeCode huffmanCode)
+ {
+ int count = 0;
+ for (int k = 0; k < huffmanCode.NumSymbols; k++)
+ {
+ if (huffmanCode.CodeLengths[k] != 0)
+ {
+ count++;
+ if (count > 1)
+ {
+ return;
+ }
+ }
+ }
+
+ for (int k = 0; k < huffmanCode.NumSymbols; k++)
+ {
+ huffmanCode.CodeLengths[k] = 0;
+ huffmanCode.Codes[k] = 0;
+ }
+ }
+
+ ///
+ /// The palette has been sorted by alpha. This function checks if the other components of the palette
+ /// have a monotonic development with regards to position in the palette.
+ /// If all have monotonic development, there is no benefit to re-organize them greedily. A monotonic development
+ /// would be spotted in green-only situations (like lossy alpha) or gray-scale images.
+ ///
+ /// The palette.
+ /// Number of colors in the palette.
+ /// True, if the palette has no monotonous deltas.
+ private static bool PaletteHasNonMonotonousDeltas(Span palette, int numColors)
+ {
+ uint predict = 0x000000;
+ byte signFound = 0x00;
+ for (int i = 0; i < numColors; ++i)
+ {
+ uint diff = LosslessUtils.SubPixels(palette[i], predict);
+ byte rd = (byte)((diff >> 16) & 0xff);
+ byte gd = (byte)((diff >> 8) & 0xff);
+ byte bd = (byte)((diff >> 0) & 0xff);
+ if (rd != 0x00)
+ {
+ signFound |= (byte)((rd < 0x80) ? 1 : 2);
+ }
+
+ if (gd != 0x00)
+ {
+ signFound |= (byte)((gd < 0x80) ? 8 : 16);
+ }
+
+ if (bd != 0x00)
+ {
+ signFound |= (byte)((bd < 0x80) ? 64 : 128);
+ }
+ }
+
+ return (signFound & (signFound << 1)) != 0; // two consequent signs.
+ }
+
+ ///
+ /// Find greedily always the closest color of the predicted color to minimize
+ /// deltas in the palette. This reduces storage needs since the palette is stored with delta encoding.
+ ///
+ /// The palette.
+ /// The number of colors in the palette.
+ private static void GreedyMinimizeDeltas(Span palette, int numColors)
+ {
+ uint predict = 0x00000000;
+ for (int i = 0; i < numColors; ++i)
+ {
+ int bestIdx = i;
+ uint bestScore = ~0U;
+ for (int k = i; k < numColors; ++k)
+ {
+ uint curScore = PaletteColorDistance(palette[k], predict);
+ if (bestScore > curScore)
+ {
+ bestScore = curScore;
+ bestIdx = k;
+ }
+ }
+
+ // Swap color(palette[bestIdx], palette[i]);
+ uint best = palette[bestIdx];
+ palette[bestIdx] = palette[i];
+ palette[i] = best;
+ predict = palette[i];
+ }
+ }
+
+ private static void GetHuffBitLengthsAndCodes(List histogramImage, HuffmanTreeCode[] huffmanCodes)
+ {
+ long totalLengthSize = 0;
+ int maxNumSymbols = 0;
+
+ // Iterate over all histograms and get the aggregate number of codes used.
+ for (int i = 0; i < histogramImage.Count; i++)
+ {
+ Vp8LHistogram histo = histogramImage[i];
+ int startIdx = 5 * i;
+ for (int k = 0; k < 5; k++)
+ {
+ int numSymbols =
+ (k == 0) ? histo.NumCodes() :
+ (k == 4) ? WebPConstants.NumDistanceCodes : 256;
+ huffmanCodes[startIdx + k].NumSymbols = numSymbols;
+ totalLengthSize += numSymbols;
+ }
+ }
+
+ var end = 5 * histogramImage.Count;
+ for (int i = 0; i < end; i++)
+ {
+ int bitLength = huffmanCodes[i].NumSymbols;
+ huffmanCodes[i].Codes = new short[bitLength];
+ huffmanCodes[i].CodeLengths = new byte[bitLength];
+ if (maxNumSymbols < bitLength)
+ {
+ maxNumSymbols = bitLength;
+ }
+ }
+
+ // Create Huffman trees.
+ bool[] bufRle = new bool[maxNumSymbols];
+ var huffTree = new HuffmanTree[3 * maxNumSymbols];
+ for (int i = 0; i < huffTree.Length; i++)
+ {
+ huffTree[i] = new HuffmanTree();
+ }
+
+ for (int i = 0; i < histogramImage.Count; i++)
+ {
+ int codesStartIdx = 5 * i;
+ Vp8LHistogram histo = histogramImage[i];
+ HuffmanUtils.CreateHuffmanTree(histo.Literal, 15, bufRle, huffTree, huffmanCodes[codesStartIdx]);
+ HuffmanUtils.CreateHuffmanTree(histo.Red, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 1]);
+ HuffmanUtils.CreateHuffmanTree(histo.Blue, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 2]);
+ HuffmanUtils.CreateHuffmanTree(histo.Alpha, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 3]);
+ HuffmanUtils.CreateHuffmanTree(histo.Distance, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 4]);
+ }
+ }
+
+ ///
+ /// Computes a value that is related to the entropy created by the palette entry diff.
+ ///
+ /// First color.
+ /// Second color.
+ /// The color distance.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static uint PaletteColorDistance(uint col1, uint col2)
+ {
+ uint diff = LosslessUtils.SubPixels(col1, col2);
+ uint moreWeightForRGBThanForAlpha = 9;
+ uint score = PaletteComponentDistance((diff >> 0) & 0xff);
+ score += PaletteComponentDistance((diff >> 8) & 0xff);
+ score += PaletteComponentDistance((diff >> 16) & 0xff);
+ score *= moreWeightForRGBThanForAlpha;
+ score += PaletteComponentDistance((diff >> 24) & 0xff);
+
+ return score;
+ }
+
+ ///
+ /// Calculates the huffman image bits.
+ ///
+ private static int GetHistoBits(int method, bool usePalette, int width, int height)
+ {
+ // Make tile size a function of encoding method (Range: 0 to 6).
+ int histoBits = (usePalette ? 9 : 7) - method;
+ while (true)
+ {
+ int huffImageSize = LosslessUtils.SubSampleSize(width, histoBits) * LosslessUtils.SubSampleSize(height, histoBits);
+ if (huffImageSize <= WebPConstants.MaxHuffImageSize)
+ {
+ break;
+ }
+
+ histoBits++;
+ }
+
+ return (histoBits < WebPConstants.MinHuffmanBits) ? WebPConstants.MinHuffmanBits :
+ (histoBits > WebPConstants.MaxHuffmanBits) ? WebPConstants.MaxHuffmanBits : histoBits;
+ }
+
+ ///
+ /// Calculates the bits used for the transformation.
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static int GetTransformBits(int method, int histoBits)
+ {
+ int maxTransformBits = (method < 4) ? 6 : (method > 4) ? 4 : 5;
+ int res = (histoBits > maxTransformBits) ? maxTransformBits : histoBits;
+ return res;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static Bgra32 ToBgra32(TPixel color)
+ where TPixel : unmanaged, IPixel
+ {
+ Rgba32 rgba = default;
+ color.ToRgba32(ref rgba);
+ var bgra = new Bgra32(rgba.R, rgba.G, rgba.B, rgba.A);
+ return bgra;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static void AddSingle(uint p, Span a, Span r, Span g, Span b)
+ {
+ a[(int)(p >> 24) & 0xff]++;
+ r[(int)(p >> 16) & 0xff]++;
+ g[(int)(p >> 8) & 0xff]++;
+ b[(int)(p >> 0) & 0xff]++;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static void AddSingleSubGreen(uint p, Span r, Span b)
+ {
+ int green = (int)p >> 8; // The upper bits are masked away later.
+ r[(int)((p >> 16) - green) & 0xff]++;
+ b[(int)((p >> 0) - green) & 0xff]++;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static uint ApplyPaletteHash0(uint color)
+ {
+ // Focus on the green color.
+ return (color >> 8) & 0xff;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static uint ApplyPaletteHash1(uint color)
+ {
+ // Forget about alpha.
+ return ((uint)((color & 0x00ffffffu) * 4222244071ul)) >> (32 - PaletteInvSizeBits);
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static uint ApplyPaletteHash2(uint color)
+ {
+ // Forget about alpha.
+ return ((uint)((color & 0x00ffffffu) * ((1ul << 31) - 1))) >> (32 - PaletteInvSizeBits);
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static uint HashPix(uint pix)
+ {
+ // Note that masking with 0xffffffffu is for preventing an
+ // 'unsigned int overflow' warning. Doesn't impact the compiled code.
+ return (uint)((((long)pix + (pix >> 19)) * 0x39c5fba7L) & 0xffffffffu) >> 24;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static int PaletteCompareColorsForSort(uint p1, uint p2)
+ {
+ return (p1 < p2) ? -1 : 1;
+ }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static uint PaletteComponentDistance(uint v)
+ {
+ return (v <= 128) ? v : (256 - v);
+ }
+
public void AllocateTransformBuffer(int width, int height)
{
int imageSize = width * height;
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
new file mode 100644
index 0000000000..546de66240
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using SixLabors.ImageSharp.Formats.WebP.BitWriter;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossy
+{
+ ///
+ /// Encoder for lossy webp images.
+ ///
+ internal class Vp8Encoder
+ {
+ ///
+ /// The to use for buffer allocations.
+ ///
+ private MemoryAllocator memoryAllocator;
+
+ ///
+ /// A bit writer for writing lossy webp streams.
+ ///
+ private Vp8BitWriter bitWriter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memory allocator.
+ /// The width of the input image.
+ /// The height of the input image.
+ public Vp8Encoder(MemoryAllocator memoryAllocator, int width, int height)
+ {
+ this.memoryAllocator = memoryAllocator;
+
+ // TODO: initialize bitwriter
+ }
+
+ public void Encode(Image image, Stream stream)
+ where TPixel : unmanaged, IPixel
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/WebPEncoder.cs b/src/ImageSharp/Formats/WebP/WebPEncoder.cs
index 3e03724f30..0100c648db 100644
--- a/src/ImageSharp/Formats/WebP/WebPEncoder.cs
+++ b/src/ImageSharp/Formats/WebP/WebPEncoder.cs
@@ -1,14 +1,15 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the GNU Affero General Public License, Version 3.
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
using System.IO;
+using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.WebP
{
///
- /// Image encoder for writing an image to a stream in the WebP format.
+ /// Image encoder for writing an image to a stream in the WebP format.
///
public sealed class WebPEncoder : IImageEncoder, IWebPEncoderOptions
{
@@ -34,5 +35,13 @@ namespace SixLabors.ImageSharp.Formats.WebP
var encoder = new WebPEncoderCore(this, image.GetMemoryAllocator());
encoder.Encode(image, stream);
}
+
+ ///
+ public Task EncodeAsync(Image image, Stream stream)
+ where TPixel : unmanaged, IPixel
+ {
+ var encoder = new WebPEncoderCore(this, image.GetMemoryAllocator());
+ return encoder.EncodeAsync(image, stream);
+ }
}
}
diff --git a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
index d7fc395b13..2d37b802a3 100644
--- a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
+++ b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
@@ -1,14 +1,11 @@
-// Copyright (c) Six Labors and contributors.
-// Licensed under the GNU Affero General Public License, Version 3.
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
-using System;
-using System.Collections.Generic;
using System.IO;
-using System.Runtime.CompilerServices;
-
+using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
-using SixLabors.ImageSharp.Formats.WebP.BitWriter;
using SixLabors.ImageSharp.Formats.WebP.Lossless;
+using SixLabors.ImageSharp.Formats.WebP.Lossy;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@@ -31,15 +28,19 @@ namespace SixLabors.ImageSharp.Formats.WebP
private Configuration configuration;
///
- /// A bit writer for writing lossless webp streams.
+ /// Indicating whether the alpha plane should be compressed with WebP lossless format.
///
- private Vp8LBitWriter bitWriter;
-
- private const int ApplyPaletteGreedyMax = 4;
+ private bool alphaCompression;
- private const int PaletteInvSizeBits = 11;
+ ///
+ /// Indicating whether lossless compression should be used. If false, lossy compression will be used.
+ ///
+ private bool lossless;
- private const int PaletteInvSize = 1 << PaletteInvSizeBits;
+ ///
+ /// Compression quality. Between 0 and 100.
+ ///
+ private float quality;
///
/// Initializes a new instance of the class.
@@ -49,6 +50,9 @@ namespace SixLabors.ImageSharp.Formats.WebP
public WebPEncoderCore(IWebPEncoderOptions options, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
+ this.alphaCompression = options.AlphaCompression;
+ this.lossless = options.Lossless;
+ this.quality = options.Quality;
}
///
@@ -66,1339 +70,40 @@ namespace SixLabors.ImageSharp.Formats.WebP
this.configuration = image.GetConfiguration();
ImageMetadata metadata = image.Metadata;
- int width = image.Width;
- int height = image.Height;
- int initialSize = width * height * 2;
- this.bitWriter = new Vp8LBitWriter(initialSize);
-
- // Write image size.
- this.WriteImageSize(width, height);
-
- // Write the non-trivial Alpha flag and lossless version.
- bool hasAlpha = false; // TODO: for the start, this will be always false.
- this.WriteAlphaAndVersion(hasAlpha);
-
- // Encode the main image stream.
- this.EncodeStream(image);
- }
-
- ///
- /// Writes the image size to the stream.
- ///
- /// The input image width.
- /// The input image height.
- private void WriteImageSize(int inputImgWidth, int inputImgHeight)
- {
- Guard.MustBeLessThan(inputImgWidth, WebPConstants.MaxDimension, nameof(inputImgWidth));
- Guard.MustBeLessThan(inputImgHeight, WebPConstants.MaxDimension, nameof(inputImgHeight));
-
- uint width = (uint)inputImgWidth - 1;
- uint height = (uint)inputImgHeight - 1;
-
- this.bitWriter.PutBits(width, WebPConstants.Vp8LImageSizeBits);
- this.bitWriter.PutBits(height, WebPConstants.Vp8LImageSizeBits);
- }
-
- private void WriteAlphaAndVersion(bool hasAlpha)
- {
- this.bitWriter.PutBits(hasAlpha ? 1U : 0, 1);
- this.bitWriter.PutBits(WebPConstants.Vp8LVersion, WebPConstants.Vp8LVersionBits);
- }
-
- ///
- /// Encodes the image stream using lossless webp format.
- ///
- /// The pixel type.
- /// The image to encode.
- private void EncodeStream(Image image)
- where TPixel : unmanaged, IPixel
- {
- int width = image.Width;
- int height = image.Height;
- int bytePosition = this.bitWriter.NumBytes();
- var enc = new Vp8LEncoder(this.memoryAllocator, width, height);
-
- // Convert image pixels to bgra array.
- Span bgra = enc.Bgra.GetSpan();
- 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;
- }
- }
-
- // Analyze image (entropy, numPalettes etc).
- this.EncoderAnalyze(image, enc, bgra);
-
- var entropyIdx = 3; // TODO: hardcoded for now.
- int quality = 75; // TODO: quality is hardcoded for now.
- bool useCache = true; // TODO: useCache is hardcoded for now.
- bool redAndBlueAlwaysZero = false;
-
- enc.UsePalette = entropyIdx == (int)EntropyIx.Palette;
- enc.UseSubtractGreenTransform = (entropyIdx == (int)EntropyIx.SubGreen) || (entropyIdx == (int)EntropyIx.SpatialSubGreen);
- enc.UsePredictorTransform = (entropyIdx == (int)EntropyIx.Spatial) || (entropyIdx == (int)EntropyIx.SpatialSubGreen);
- enc.UseCrossColorTransform = redAndBlueAlwaysZero ? false : enc.UsePredictorTransform;
- enc.AllocateTransformBuffer(width, height);
-
- // Reset any parameter in the encoder that is set in the previous iteration.
- enc.CacheBits = 0;
- enc.ClearRefs();
-
- // TODO: Apply near-lossless preprocessing.
-
- // Encode palette.
- if (enc.UsePalette)
- {
- this.EncodePalette(image, bgra, enc);
- this.MapImageFromPalette(enc, width, height);
-
- // If using a color cache, do not have it bigger than the number of colors.
- if (useCache && enc.PaletteSize < (1 << WebPConstants.MaxColorCacheBits))
- {
- enc.CacheBits = WebPCommonUtils.BitsLog2Floor((uint)enc.PaletteSize) + 1;
- }
- }
-
- // Apply transforms and write transform data.
- if (enc.UseSubtractGreenTransform)
- {
- this.ApplySubtractGreen(enc, enc.CurrentWidth, height);
- }
-
- if (enc.UsePredictorTransform)
- {
- this.ApplyPredictFilter(enc, enc.CurrentWidth, height, quality, enc.UseSubtractGreenTransform);
- }
-
- if (enc.UseCrossColorTransform)
- {
- this.ApplyCrossColorFilter(enc, enc.CurrentWidth, height, quality);
- }
-
- this.bitWriter.PutBits(0, 1); // No more transforms.
-
- // Encode and write the transformed image.
- this.EncodeImage(bgra, enc.HashChain, enc.Refs, enc.CurrentWidth, height, quality, useCache, enc.CacheBits, enc.HistoBits, bytePosition);
- }
-
- ///
- /// Analyzes the image and decides what transforms should be used.
- ///
- private void EncoderAnalyze(Image image, Vp8LEncoder enc, Span bgra)
- where TPixel : unmanaged, IPixel
- {
- int method = 4; // TODO: method hardcoded to 4 for now.
- 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.
- enc.HistoBits = GetHistoBits(method, usePalette, width, height);
- enc.TransformBits = GetTransformBits(method, enc.HistoBits);
-
- // Try out multiple LZ77 on images with few colors.
- var nlz77s = (enc.PaletteSize > 0 && enc.PaletteSize <= 16) ? 2 : 1;
- EntropyIx entropyIdx = this.AnalyzeEntropy(image, usePalette, enc.PaletteSize, enc.TransformBits, out bool redAndBlueAlwaysZero);
-
- // TODO: Fill CrunchConfig
- }
-
- private void EncodeImage(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs[] refsArray, int width, int height, int quality, bool useCache, int cacheBits, int histogramBits, int initBytePosition)
- {
- int lz77sTypesToTrySize = 1; // TODO: hardcoded for now.
- int[] lz77sTypesToTry = { 3 };
- int histogramImageXySize = LosslessUtils.SubSampleSize(width, histogramBits) * LosslessUtils.SubSampleSize(height, histogramBits);
- var histogramSymbols = new short[histogramImageXySize];
- var huffTree = new HuffmanTree[3 * WebPConstants.CodeLengthCodes];
- for (int i = 0; i < huffTree.Length; i++)
- {
- huffTree[i] = new HuffmanTree();
- }
-
- if (useCache)
- {
- if (cacheBits == 0)
- {
- cacheBits = WebPConstants.MaxColorCacheBits;
- }
- }
- else
- {
- cacheBits = 0;
- }
-
- // Calculate backward references from ARGB image.
- BackwardReferenceEncoder.HashChainFill(hashChain, bgra, quality, width, height);
- // TODO: BitWriterInit(&bw_best, 0)
- // BitWriterClone(bw, &bw_best))
-
- for (int lz77sIdx = 0; lz77sIdx < lz77sTypesToTrySize; lz77sIdx++)
- {
- Vp8LBackwardRefs refsBest = BackwardReferenceEncoder.GetBackwardReferences(width, height, bgra, quality, lz77sTypesToTry[lz77sIdx], ref cacheBits, hashChain, refsArray[0], refsArray[1]);
-
- // Keep the best references aside and use the other element from the first
- // two as a temporary for later usage.
- Vp8LBackwardRefs refsTmp = refsArray[refsBest.Equals(refsArray[0]) ? 1 : 0];
-
- var tmpHisto = new Vp8LHistogram(cacheBits);
- var histogramImage = new List(histogramImageXySize);
- for (int i = 0; i < histogramImageXySize; i++)
- {
- histogramImage.Add(new Vp8LHistogram(cacheBits));
- }
-
- // Build histogram image and symbols from backward references.
- HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, quality, histogramBits, cacheBits, histogramImage, tmpHisto, histogramSymbols);
-
- // Create Huffman bit lengths and codes for each histogram image.
- var histogramImageSize = histogramImage.Count;
- var bitArraySize = 5 * histogramImageSize;
- var huffmanCodes = new HuffmanTreeCode[bitArraySize];
- for (int i = 0; i < huffmanCodes.Length; i++)
- {
- huffmanCodes[i] = new HuffmanTreeCode();
- }
-
- GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
-
- // Color Cache parameters.
- if (cacheBits > 0)
- {
- this.bitWriter.PutBits(1, 1);
- this.bitWriter.PutBits((uint)cacheBits, 4);
- }
- else
- {
- this.bitWriter.PutBits(0, 1);
- }
-
- // Huffman image + meta huffman.
- bool writeHistogramImage = histogramImageSize > 1;
- this.bitWriter.PutBits((uint)(writeHistogramImage ? 1 : 0), 1);
- if (writeHistogramImage)
- {
- using System.Buffers.IMemoryOwner histogramArgbBuffer = this.memoryAllocator.Allocate(histogramImageXySize);
- Span histogramArgb = histogramArgbBuffer.GetSpan();
- int maxIndex = 0;
- for (int i = 0; i < histogramImageXySize; i++)
- {
- int symbolIndex = histogramSymbols[i] & 0xffff;
- histogramArgb[i] = (uint)(symbolIndex << 8);
- if (symbolIndex >= maxIndex)
- {
- maxIndex = symbolIndex + 1;
- }
- }
-
- histogramImageSize = maxIndex;
- this.bitWriter.PutBits((uint)(histogramBits - 2), 3);
- this.EncodeImageNoHuffman(histogramArgb, hashChain, refsTmp, refsArray[2], LosslessUtils.SubSampleSize(width, histogramBits), LosslessUtils.SubSampleSize(height, histogramBits), quality);
- }
-
- // Store Huffman codes.
- // Find maximum number of symbols for the huffman tree-set.
- int maxTokens = 0;
- for (int i = 0; i < 5 * histogramImageSize; i++)
- {
- HuffmanTreeCode codes = huffmanCodes[i];
- if (maxTokens < codes.NumSymbols)
- {
- maxTokens = codes.NumSymbols;
- }
- }
-
- var tokens = new HuffmanTreeToken[maxTokens];
- for (int i = 0; i < tokens.Length; i++)
- {
- tokens[i] = new HuffmanTreeToken();
- }
-
- for (int i = 0; i < 5 * histogramImageSize; i++)
- {
- HuffmanTreeCode codes = huffmanCodes[i];
- this.StoreHuffmanCode(huffTree, tokens, codes);
- ClearHuffmanTreeIfOnlyOneSymbol(codes);
- }
-
- // Store actual literals.
- var hdrSizeTmp = (int)(this.bitWriter.NumBytes() - initBytePosition);
- this.StoreImageToBitMask(width, histogramBits, refsBest, histogramSymbols, huffmanCodes);
-
- // TODO: Keep track of the smallest image so far.
- }
- }
-
- ///
- /// Save the palette to the bitstream.
- ///
- /// The image.
- /// The Vp8L Encoder.
- private void EncodePalette(Image image, Span bgra, Vp8LEncoder enc)
- where TPixel : unmanaged, IPixel
- {
- Span 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(tmpPalette, enc.HashChain, enc.Refs[0], enc.Refs[1], width: paletteSize, height: 1, quality: 20);
- }
-
- ///
- /// Applies the substract green transformation to the pixel data of the image.
- ///
- /// The VP8L Encoder.
- /// The width of the image.
- /// The height of the image.
- private void ApplySubtractGreen(Vp8LEncoder enc, int width, int height)
- {
- this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
- this.bitWriter.PutBits((uint)Vp8LTransformType.SubtractGreen, 2);
- LosslessUtils.SubtractGreenFromBlueAndRed(enc.Bgra.GetSpan(), width * height);
- }
-
- private void ApplyPredictFilter(Vp8LEncoder enc, int width, int height, int quality, bool usedSubtractGreen)
- {
- int nearLosslessStrength = 100; // TODO: for now always 100
- bool exact = false; // TODO: always false for now.
- int predBits = enc.TransformBits;
- int transformWidth = LosslessUtils.SubSampleSize(width, predBits);
- int transformHeight = LosslessUtils.SubSampleSize(height, predBits);
-
- PredictorEncoder.ResidualImage(width, height, predBits, enc.Bgra.GetSpan(), enc.BgraScratch.GetSpan(), enc.TransformData.GetSpan(), nearLosslessStrength, exact, usedSubtractGreen);
-
- this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
- this.bitWriter.PutBits((uint)Vp8LTransformType.PredictorTransform, 2);
- this.bitWriter.PutBits((uint)(predBits - 2), 3);
-
- this.EncodeImageNoHuffman(enc.TransformData.GetSpan(), enc.HashChain, enc.Refs[0], enc.Refs[1], transformWidth, transformHeight, quality);
- }
-
- private void ApplyCrossColorFilter(Vp8LEncoder enc, int width, int height, int quality)
- {
- int colorTransformBits = enc.TransformBits;
- int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits);
- int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits);
-
- PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, quality, enc.Bgra.GetSpan(), enc.TransformData.GetSpan());
-
- this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
- this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2);
- this.bitWriter.PutBits((uint)(colorTransformBits - 2), 3);
-
- this.EncodeImageNoHuffman(enc.TransformData.GetSpan(), enc.HashChain, enc.Refs[0], enc.Refs[1], transformWidth, transformHeight, quality);
- }
-
- private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs refsTmp1, Vp8LBackwardRefs refsTmp2, int width, int height, int quality)
- {
- int cacheBits = 0;
- var histogramSymbols = new short[1]; // Only one tree, one symbol.
- var huffmanCodes = new HuffmanTreeCode[5];
- for (int i = 0; i < huffmanCodes.Length; i++)
- {
- huffmanCodes[i] = new HuffmanTreeCode();
- }
-
- var huffTree = new HuffmanTree[3UL * WebPConstants.CodeLengthCodes];
- for (int i = 0; i < huffTree.Length; i++)
- {
- huffTree[i] = new HuffmanTree();
- }
-
- // Calculate backward references from the image pixels.
- BackwardReferenceEncoder.HashChainFill(hashChain, bgra, quality, width, height);
-
- Vp8LBackwardRefs refs = BackwardReferenceEncoder.GetBackwardReferences(
- width,
- height,
- bgra,
- quality,
- (int)Vp8LLz77Type.Lz77Standard | (int)Vp8LLz77Type.Lz77Rle,
- ref cacheBits,
- hashChain,
- refsTmp1,
- refsTmp2);
-
- var histogramImage = new List()
- {
- new Vp8LHistogram(cacheBits)
- };
-
- // Build histogram image and symbols from backward references.
- histogramImage[0].StoreRefs(refs);
-
- // Create Huffman bit lengths and codes for each histogram image.
- GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
-
- // No color cache, no Huffman image.
- this.bitWriter.PutBits(0, 1);
-
- // Find maximum number of symbols for the huffman tree-set.
- int maxTokens = 0;
- for (int i = 0; i < 5; i++)
- {
- HuffmanTreeCode codes = huffmanCodes[i];
- if (maxTokens < codes.NumSymbols)
- {
- maxTokens = codes.NumSymbols;
- }
- }
-
- var tokens = new HuffmanTreeToken[maxTokens];
- for (int i = 0; i < tokens.Length; i++)
- {
- tokens[i] = new HuffmanTreeToken();
- }
-
- // Store Huffman codes.
- for (int i = 0; i < 5; i++)
- {
- HuffmanTreeCode codes = huffmanCodes[i];
- this.StoreHuffmanCode(huffTree, tokens, codes);
- ClearHuffmanTreeIfOnlyOneSymbol(codes);
- }
-
- // Store actual literals.
- this.StoreImageToBitMask(width, 0, refs, histogramSymbols, huffmanCodes);
- }
-
- private void StoreHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode)
- {
- int count = 0;
- int[] symbols = { 0, 0 };
- int maxBits = 8;
- int maxSymbol = 1 << maxBits;
-
- // Check whether it's a small tree.
- for (int i = 0; i < huffmanCode.NumSymbols && count < 3; ++i)
- {
- if (huffmanCode.CodeLengths[i] != 0)
- {
- if (count < 2)
- {
- symbols[count] = i;
- }
-
- count++;
- }
- }
-
- if (count == 0)
+ if (this.lossless)
{
- // emit minimal tree for empty cases
- // bits: small tree marker: 1, count-1: 0, large 8-bit code: 0, code: 0
- this.bitWriter.PutBits(0x01, 4);
- }
- else if (count <= 2 && symbols[0] < maxSymbol && symbols[1] < maxSymbol)
- {
- this.bitWriter.PutBits(1, 1); // Small tree marker to encode 1 or 2 symbols.
- this.bitWriter.PutBits((uint)(count - 1), 1);
- if (symbols[0] <= 1)
- {
- this.bitWriter.PutBits(0, 1); // Code bit for small (1 bit) symbol value.
- this.bitWriter.PutBits((uint)symbols[0], 1);
- }
- else
- {
- this.bitWriter.PutBits(1, 1);
- this.bitWriter.PutBits((uint)symbols[0], 8);
- }
-
- if (count == 2)
- {
- this.bitWriter.PutBits((uint)symbols[1], 8);
- }
+ var enc = new Vp8LEncoder(this.memoryAllocator, image.Width, image.Height);
+ enc.Encode(image, stream);
}
else
{
- this.StoreFullHuffmanCode(huffTree, tokens, huffmanCode);
- }
- }
-
- private void StoreFullHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree)
- {
- int i;
- var codeLengthBitDepth = new byte[WebPConstants.CodeLengthCodes];
- var codeLengthBitDepthSymbols = new short[WebPConstants.CodeLengthCodes];
- var huffmanCode = new HuffmanTreeCode();
- huffmanCode.NumSymbols = WebPConstants.CodeLengthCodes;
- huffmanCode.CodeLengths = codeLengthBitDepth;
- huffmanCode.Codes = codeLengthBitDepthSymbols;
-
- this.bitWriter.PutBits(0, 1);
- var numTokens = HuffmanUtils.CreateCompressedHuffmanTree(tree, tokens);
- var histogram = new uint[WebPConstants.CodeLengthCodes + 1];
- var bufRle = new bool[WebPConstants.CodeLengthCodes + 1];
- for (i = 0; i < numTokens; i++)
- {
- histogram[tokens[i].Code]++;
- }
-
- HuffmanUtils.CreateHuffmanTree(histogram, 7, bufRle, huffTree, huffmanCode);
- this.StoreHuffmanTreeOfHuffmanTreeToBitMask(codeLengthBitDepth);
- ClearHuffmanTreeIfOnlyOneSymbol(huffmanCode);
-
- int trailingZeroBits = 0;
- int trimmedLength = numTokens;
- i = numTokens;
- while (i-- > 0)
- {
- int ix = tokens[i].Code;
- if (ix == 0 || ix == 17 || ix == 18)
- {
- trimmedLength--; // discount trailing zeros.
- trailingZeroBits += codeLengthBitDepth[ix];
- if (ix == 17)
- {
- trailingZeroBits += 3;
- }
- else if (ix == 18)
- {
- trailingZeroBits += 7;
- }
- }
- else
- {
- break;
- }
- }
-
- var writeTrimmedLength = trimmedLength > 1 && trailingZeroBits > 12;
- var length = writeTrimmedLength ? trimmedLength : numTokens;
- this.bitWriter.PutBits((uint)(writeTrimmedLength ? 1 : 0), 1);
- if (writeTrimmedLength)
- {
- if (trimmedLength == 2)
- {
- this.bitWriter.PutBits(0, 3 + 2); // nbitpairs=1, trimmed_length=2
- }
- else
- {
- int nbits = WebPCommonUtils.BitsLog2Floor((uint)trimmedLength - 2);
- int nbitpairs = (nbits / 2) + 1;
- this.bitWriter.PutBits((uint)nbitpairs - 1, 3);
- this.bitWriter.PutBits((uint)trimmedLength - 2, nbitpairs * 2);
- }
- }
-
- this.StoreHuffmanTreeToBitMask(tokens, length, huffmanCode);
- }
-
- private void StoreHuffmanTreeToBitMask(HuffmanTreeToken[] tokens, int numTokens, HuffmanTreeCode huffmanCode)
- {
- for (int i = 0; i < numTokens; i++)
- {
- int ix = tokens[i].Code;
- int extraBits = tokens[i].ExtraBits;
- this.bitWriter.PutBits((uint)huffmanCode.Codes[ix], huffmanCode.CodeLengths[ix]);
- switch (ix)
- {
- case 16:
- this.bitWriter.PutBits((uint)extraBits, 2);
- break;
- case 17:
- this.bitWriter.PutBits((uint)extraBits, 3);
- break;
- case 18:
- this.bitWriter.PutBits((uint)extraBits, 7);
- break;
- }
- }
- }
-
- private void StoreHuffmanTreeOfHuffmanTreeToBitMask(byte[] codeLengthBitdepth)
- {
- // RFC 1951 will calm you down if you are worried about this funny sequence.
- // This sequence is tuned from that, but more weighted for lower symbol count,
- // and more spiking histograms.
- byte[] storageOrder = { 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
-
- // Throw away trailing zeros:
- int codesToStore = WebPConstants.CodeLengthCodes;
- for (; codesToStore > 4; codesToStore--)
- {
- if (codeLengthBitdepth[storageOrder[codesToStore - 1]] != 0)
- {
- break;
- }
- }
-
- this.bitWriter.PutBits((uint)codesToStore - 4, 4);
- for (int i = 0; i < codesToStore; i++)
- {
- this.bitWriter.PutBits(codeLengthBitdepth[storageOrder[i]], 3);
- }
- }
-
- private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, 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);
- using List.Enumerator c = backwardRefs.Refs.GetEnumerator();
- while (c.MoveNext())
- {
- PixOrCopy v = c.Current;
- if ((tileX != (x & tileMask)) || (tileY != (y & tileMask)))
- {
- tileX = x & tileMask;
- tileY = y & tileMask;
- histogramIx = histogramSymbols[((y >> histoBits) * histoXSize) + (x >> histoBits)];
- codes = huffmanCodes.AsSpan(5 * histogramIx);
- }
-
- if (v.IsLiteral())
- {
- byte[] order = { 1, 2, 0, 3 };
- for (int k = 0; k < 4; k++)
- {
- int code = (int)v.Literal(order[k]);
- this.bitWriter.WriteHuffmanCode(codes[k], code);
- }
- }
- else if (v.IsCacheIdx())
- {
- int code = (int)v.CacheIdx();
- int literalIx = 256 + WebPConstants.NumLengthCodes + code;
- this.bitWriter.WriteHuffmanCode(codes[0], literalIx);
- }
- else
- {
- int bits = 0;
- int nBits = 0;
- int distance = (int)v.Distance();
- int code = LosslessUtils.PrefixEncode(v.Len, ref nBits, ref bits);
- this.bitWriter.WriteHuffmanCodeWithExtraBits(codes[0], 256 + code, bits, nBits);
-
- // Don't write the distance with the extra bits code since
- // the distance can be up to 18 bits of extra bits, and the prefix
- // 15 bits, totaling to 33, and our PutBits only supports up to 32 bits.
- code = LosslessUtils.PrefixEncode(distance, ref nBits, ref bits);
- this.bitWriter.WriteHuffmanCode(codes[4], code);
- this.bitWriter.PutBits((uint)bits, nBits);
- }
-
- x += v.Length();
- while (x >= width)
- {
- x -= width;
- y++;
- }
- }
- }
-
- ///
- /// Analyzes the entropy of the input image to determine which transforms to use during encoding the image.
- ///
- /// The pixel type of the image.
- /// The image to analyze.
- /// Indicates whether a palette should be used.
- /// The palette size.
- /// The transformation bits.
- /// Indicates if red and blue are always zero.
- /// The entropy mode to use.
- private EntropyIx AnalyzeEntropy(Image image, bool usePalette, int paletteSize, int transformBits, out bool redAndBlueAlwaysZero)
- where TPixel : unmanaged, IPixel
- {
- int width = image.Width;
- int height = image.Height;
-
- if (usePalette && paletteSize <= 16)
- {
- // In the case of small palettes, we pack 2, 4 or 8 pixels together. In
- // practice, small palettes are better than any other transform.
- redAndBlueAlwaysZero = true;
- return EntropyIx.Palette;
- }
-
- using System.Buffers.IMemoryOwner histoBuffer = this.memoryAllocator.Allocate((int)HistoIx.HistoTotal * 256);
- Span histo = histoBuffer.Memory.Span;
- Bgra32 pixPrev = ToBgra32(image.GetPixelRowSpan(0)[0]); // Skip the first pixel.
- Span prevRow = null;
- for (int y = 0; y < height; y++)
- {
- Span currentRow = image.GetPixelRowSpan(y);
- for (int x = 0; x < width; x++)
- {
- Bgra32 pix = ToBgra32(currentRow[x]);
- uint pixDiff = LosslessUtils.SubPixels(pix.PackedValue, pixPrev.PackedValue);
- pixPrev = pix;
- if ((pixDiff == 0) || (prevRow != null && pix == ToBgra32(prevRow[x])))
- {
- continue;
- }
-
- AddSingle(
- pix.PackedValue,
- histo.Slice((int)HistoIx.HistoAlpha * 256),
- histo.Slice((int)HistoIx.HistoRed * 256),
- histo.Slice((int)HistoIx.HistoGreen * 256),
- histo.Slice((int)HistoIx.HistoBlue * 256));
- AddSingle(
- pixDiff,
- 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.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]++;
- }
-
- var histo0 = histo[0];
- prevRow = currentRow;
- }
-
- var entropyComp = new double[(int)HistoIx.HistoTotal];
- var entropy = new double[(int)EntropyIx.NumEntropyIx];
- 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 pixDiff == 0 comparison, at least one of the
- // zeros is likely to exist.
- histo[(int)HistoIx.HistoRedPredSubGreen * 256]++;
- histo[(int)HistoIx.HistoBluePredSubGreen * 256]++;
- histo[(int)HistoIx.HistoRedPred * 256]++;
- histo[(int)HistoIx.HistoGreenPred * 256]++;
- histo[(int)HistoIx.HistoBluePred * 256]++;
- histo[(int)HistoIx.HistoAlphaPred * 256]++;
-
- for (int j = 0; j < (int)HistoIx.HistoTotal; ++j)
- {
- var bitEntropy = new Vp8LBitEntropy();
- Span curHisto = histo.Slice(j * 256, 256);
- bitEntropy.BitsEntropyUnrefined(curHisto, 256);
- entropyComp[j] = bitEntropy.BitsEntropyRefine();
- }
-
- entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] +
- entropyComp[(int)HistoIx.HistoRed] +
- entropyComp[(int)HistoIx.HistoGreen] +
- entropyComp[(int)HistoIx.HistoBlue];
- entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] +
- entropyComp[(int)HistoIx.HistoRedPred] +
- entropyComp[(int)HistoIx.HistoGreenPred] +
- entropyComp[(int)HistoIx.HistoBluePred];
- entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] +
- entropyComp[(int)HistoIx.HistoRedSubGreen] +
- entropyComp[(int)HistoIx.HistoGreen] +
- entropyComp[(int)HistoIx.HistoBlueSubGreen];
- entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] +
- entropyComp[(int)HistoIx.HistoRedPredSubGreen] +
- entropyComp[(int)HistoIx.HistoGreenPred] +
- entropyComp[(int)HistoIx.HistoBluePredSubGreen];
- entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette];
-
- // When including transforms, there is an overhead in bits from
- // storing them. This overhead is small but matters for small images.
- // For spatial, there are 14 transformations.
- entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) *
- LosslessUtils.SubSampleSize(height, transformBits) *
- LosslessUtils.FastLog2(14);
-
- // 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);
-
- // For palettes, add the cost of storing the palette.
- // We empirically estimate the cost of a compressed entry as 8 bits.
- // The palette is differential-coded when compressed hence a much
- // lower cost than sizeof(uint32_t)*8.
- entropy[(int)EntropyIx.Palette] += paletteSize * 8;
-
- EntropyIx minEntropyIx = EntropyIx.Direct;
- for (int k = (int)EntropyIx.Direct + 1; k <= lastModeToAnalyze; k++)
- {
- if (entropy[(int)minEntropyIx] > entropy[k])
- {
- minEntropyIx = (EntropyIx)k;
- }
- }
-
- redAndBlueAlwaysZero = true;
-
- // Let's check if the histogram of the chosen entropy mode has
- // non-zero red and blue values. If all are zero, we can later skip
- // the cross color optimization.
- var histoPairs = new byte[][]
- {
- new byte[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue },
- new byte[] { (byte)HistoIx.HistoRedPred, (byte)HistoIx.HistoBluePred },
- new byte[] { (byte)HistoIx.HistoRedSubGreen, (byte)HistoIx.HistoBlueSubGreen },
- new byte[] { (byte)HistoIx.HistoRedPredSubGreen, (byte)HistoIx.HistoBluePredSubGreen },
- new byte[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue }
- };
- Span redHisto = histo.Slice(256 * histoPairs[(int)minEntropyIx][0]);
- Span blueHisto = histo.Slice(256 * histoPairs[(int)minEntropyIx][1]);
- for (int i = 1; i < 256; i++)
- {
- if ((redHisto[i] | blueHisto[i]) != 0)
- {
- redAndBlueAlwaysZero = false;
- break;
- }
- }
-
- return minEntropyIx;
- }
-
- ///
- /// If number of colors in the image is less than or equal to MAX_PALETTE_SIZE,
- /// creates a palette and returns true, else returns false.
- ///
- /// true, if a palette should be used.
- private bool AnalyzeAndCreatePalette(Image image, Vp8LEncoder enc)
- where TPixel : unmanaged, IPixel
- {
- Span palette = enc.Palette.Memory.Span;
- enc.PaletteSize = this.GetColorPalette(image, palette);
- if (enc.PaletteSize > WebPConstants.MaxPaletteSize)
- {
- enc.PaletteSize = 0;
- return false;
- }
-
- uint[] paletteArray = palette.Slice(0, enc.PaletteSize).ToArray();
- Array.Sort(paletteArray);
- paletteArray.CopyTo(palette);
-
- if (PaletteHasNonMonotonousDeltas(palette, enc.PaletteSize))
- {
- GreedyMinimizeDeltas(palette, enc.PaletteSize);
+ var enc = new Vp8Encoder(this.memoryAllocator, image.Width, image.Height);
+ enc.Encode(image, stream);
}
-
- return true;
}
///
- /// Gets the color palette.
+ /// Encodes the image to the specified stream from the .
///
- /// The pixel type of the image.
- /// The image to get the palette from.
- /// The span to store the palette into.
- /// The number of palette entries.
- private int GetColorPalette(Image image, Span palette)
+ /// The pixel format.
+ /// The to encode from.
+ /// The to encode the image data to.
+ public async Task EncodeAsync(Image image, Stream stream)
where TPixel : unmanaged, IPixel
{
- var colors = new HashSet();
- for (int y = 0; y < image.Height; y++)
+ if (stream.CanSeek)
{
- Span rowSpan = image.GetPixelRowSpan(y);
- for (int x = 0; x < rowSpan.Length; x++)
- {
- colors.Add(rowSpan[x]);
- if (colors.Count > WebPConstants.MaxPaletteSize)
- {
- // Exact count is not needed, because a palette will not be used then anyway.
- return WebPConstants.MaxPaletteSize + 1;
- }
- }
- }
-
- // Fill the colors into the palette.
- using HashSet.Enumerator colorEnumerator = colors.GetEnumerator();
- int idx = 0;
- while (colorEnumerator.MoveNext())
- {
- Bgra32 bgra = ToBgra32(colorEnumerator.Current);
- palette[idx++] = bgra.PackedValue;
- }
-
- return colors.Count;
- }
-
- private void MapImageFromPalette(Vp8LEncoder enc, int width, int height)
- {
- Span src = enc.Bgra.GetSpan();
- int srcStride = enc.CurrentWidth;
- Span dst = enc.Bgra.GetSpan(); // Applying the palette will be done in place.
- Span palette = enc.Palette.GetSpan();
- int paletteSize = enc.PaletteSize;
- int xBits;
-
- // Replace each input pixel by corresponding palette index.
- // This is done line by line.
- if (paletteSize <= 4)
- {
- xBits = (paletteSize <= 2) ? 3 : 2;
- }
- else
- {
- xBits = (paletteSize <= 16) ? 1 : 0;
- }
-
- this.ApplyPalette(src, srcStride, dst, enc.CurrentWidth, palette, paletteSize, width, height, xBits);
- }
-
- ///
- /// Remap argb values in src[] to packed palettes entries in dst[]
- /// using 'row' as a temporary buffer of size 'width'.
- /// We assume that all src[] values have a corresponding entry in the palette.
- /// Note: src[] can be the same as dst[]
- ///
- private void ApplyPalette(Span src, int srcStride, Span dst, int dstStride, Span palette, int paletteSize, int width, int height, int xBits)
- {
- using System.Buffers.IMemoryOwner tmpRowBuffer = this.memoryAllocator.Allocate(width);
- Span tmpRow = tmpRowBuffer.GetSpan();
-
- if (paletteSize < ApplyPaletteGreedyMax)
- {
- // TODO: APPLY_PALETTE_FOR(SearchColorGreedy(palette, palette_size, pix));
+ this.Encode(image, stream);
}
else
{
- uint[] buffer = new uint[PaletteInvSize];
-
- // Try to find a perfect hash function able to go from a color to an index
- // within 1 << PaletteInvSize in order to build a hash map to go from color to index in palette.
- int i;
- for (i = 0; i < 3; i++)
- {
- bool useLUT = true;
-
- // Set each element in buffer to max value.
- buffer.AsSpan().Fill(uint.MaxValue);
-
- for (int j = 0; j < paletteSize; j++)
- {
- uint ind = 0;
- switch (i)
- {
- case 0:
- ind = ApplyPaletteHash0(palette[j]);
- break;
- case 1:
- ind = ApplyPaletteHash1(palette[j]);
- break;
- case 2:
- ind = ApplyPaletteHash2(palette[j]);
- break;
- }
-
- if (buffer[ind] != uint.MaxValue)
- {
- useLUT = false;
- break;
- }
- else
- {
- buffer[ind] = (uint)j;
- }
- }
-
- if (useLUT)
- {
- break;
- }
- }
-
- if (i == 0 || i == 1 || i == 2)
- {
- ApplyPaletteFor(width, height, palette, i, src, srcStride, dst, dstStride, tmpRow, buffer, xBits);
- }
- else
- {
- uint[] idxMap = new uint[paletteSize];
- uint[] paletteSorted = new uint[paletteSize];
- PrepareMapToPalette(palette, paletteSize, paletteSorted, idxMap);
- ApplyPaletteForWithIdxMap(width, height, palette, src, srcStride, dst, dstStride, tmpRow, idxMap, xBits, paletteSorted, paletteSize);
- }
- }
- }
-
- private static void ApplyPaletteFor(int width, int height, Span palette, int hashIdx, Span src, int srcStride, Span dst, int dstStride, Span tmpRow, uint[] buffer, int xBits)
- {
- uint prevPix = palette[0];
- uint prevIdx = 0;
- for (int y = 0; y < height; y++)
- {
- for (int x = 0; x < width; x++)
- {
- uint pix = src[x];
- if (pix != prevPix)
- {
- switch (hashIdx)
- {
- case 0:
- prevIdx = buffer[ApplyPaletteHash0(pix)];
- break;
- case 1:
- prevIdx = buffer[ApplyPaletteHash1(pix)];
- break;
- case 2:
- prevIdx = buffer[ApplyPaletteHash2(pix)];
- break;
- }
-
- prevPix = pix;
- }
-
- tmpRow[x] = (byte)prevIdx;
- }
-
- LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst);
-
- src = src.Slice((int)srcStride);
- dst = dst.Slice((int)dstStride);
- }
- }
-
- private static void ApplyPaletteForWithIdxMap(int width, int height, Span palette, Span src, int srcStride, Span dst, int dstStride, Span tmpRow, uint[] idxMap, int xBits, uint[] paletteSorted, int paletteSize)
- {
- uint prevPix = palette[0];
- uint prevIdx = 0;
- for (int y = 0; y < height; y++)
- {
- for (int x = 0; x < width; x++)
- {
- uint pix = src[x];
- if (pix != prevPix)
- {
- prevIdx = idxMap[SearchColorNoIdx(paletteSorted, pix, paletteSize)];
- prevPix = pix;
- }
-
- tmpRow[x] = (byte)prevIdx;
- }
-
- LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst);
-
- src = src.Slice((int)srcStride);
- dst = dst.Slice((int)dstStride);
- }
- }
-
- ///
- /// Sort palette in increasing order and prepare an inverse mapping array.
- ///
- private static void PrepareMapToPalette(Span palette, int numColors, uint[] sorted, uint[] idxMap)
- {
- palette.Slice(numColors).CopyTo(sorted);
- Array.Sort(sorted, PaletteCompareColorsForSort);
- for (int i = 0; i < numColors; i++)
- {
- idxMap[SearchColorNoIdx(sorted, palette[i], numColors)] = (uint)i;
- }
- }
-
- private static int SearchColorNoIdx(uint[] sorted, uint color, int hi)
- {
- int low = 0;
- if (sorted[low] == color)
- {
- return low; // loop invariant: sorted[low] != color
- }
-
- while (true)
- {
- int mid = (low + hi) >> 1;
- if (sorted[mid] == color)
- {
- return mid;
- }
- else if (sorted[mid] < color)
- {
- low = mid;
- }
- else
- {
- hi = mid;
- }
- }
- }
-
- private static void ClearHuffmanTreeIfOnlyOneSymbol(HuffmanTreeCode huffmanCode)
- {
- int count = 0;
- for (int k = 0; k < huffmanCode.NumSymbols; k++)
- {
- if (huffmanCode.CodeLengths[k] != 0)
- {
- count++;
- if (count > 1)
- {
- return;
- }
- }
- }
-
- for (int k = 0; k < huffmanCode.NumSymbols; k++)
- {
- huffmanCode.CodeLengths[k] = 0;
- huffmanCode.Codes[k] = 0;
- }
- }
-
- ///
- /// The palette has been sorted by alpha. This function checks if the other components of the palette
- /// have a monotonic development with regards to position in the palette.
- /// If all have monotonic development, there is no benefit to re-organize them greedily. A monotonic development
- /// would be spotted in green-only situations (like lossy alpha) or gray-scale images.
- ///
- /// The palette.
- /// Number of colors in the palette.
- /// True, if the palette has no monotonous deltas.
- private static bool PaletteHasNonMonotonousDeltas(Span palette, int numColors)
- {
- uint predict = 0x000000;
- byte signFound = 0x00;
- for (int i = 0; i < numColors; ++i)
- {
- uint diff = LosslessUtils.SubPixels(palette[i], predict);
- byte rd = (byte)((diff >> 16) & 0xff);
- byte gd = (byte)((diff >> 8) & 0xff);
- byte bd = (byte)((diff >> 0) & 0xff);
- if (rd != 0x00)
- {
- signFound |= (byte)((rd < 0x80) ? 1 : 2);
- }
-
- if (gd != 0x00)
- {
- signFound |= (byte)((gd < 0x80) ? 8 : 16);
- }
-
- if (bd != 0x00)
- {
- signFound |= (byte)((bd < 0x80) ? 64 : 128);
- }
- }
-
- return (signFound & (signFound << 1)) != 0; // two consequent signs.
- }
-
- ///
- /// Find greedily always the closest color of the predicted color to minimize
- /// deltas in the palette. This reduces storage needs since the palette is stored with delta encoding.
- ///
- /// The palette.
- /// The number of colors in the palette.
- private static void GreedyMinimizeDeltas(Span palette, int numColors)
- {
- uint predict = 0x00000000;
- for (int i = 0; i < numColors; ++i)
- {
- int bestIdx = i;
- uint bestScore = ~0U;
- for (int k = i; k < numColors; ++k)
+ using (var ms = new MemoryStream())
{
- uint curScore = PaletteColorDistance(palette[k], predict);
- if (bestScore > curScore)
- {
- bestScore = curScore;
- bestIdx = k;
- }
+ this.Encode(image, ms);
+ ms.Position = 0;
+ await ms.CopyToAsync(stream).ConfigureAwait(false);
}
-
- // Swap color(palette[bestIdx], palette[i]);
- uint best = palette[bestIdx];
- palette[bestIdx] = palette[i];
- palette[i] = best;
- predict = palette[i];
}
}
-
- private static void GetHuffBitLengthsAndCodes(List histogramImage, HuffmanTreeCode[] huffmanCodes)
- {
- long totalLengthSize = 0;
- int maxNumSymbols = 0;
-
- // Iterate over all histograms and get the aggregate number of codes used.
- for (int i = 0; i < histogramImage.Count; i++)
- {
- Vp8LHistogram histo = histogramImage[i];
- int startIdx = 5 * i;
- for (int k = 0; k < 5; k++)
- {
- int numSymbols =
- (k == 0) ? histo.NumCodes() :
- (k == 4) ? WebPConstants.NumDistanceCodes : 256;
- huffmanCodes[startIdx + k].NumSymbols = numSymbols;
- totalLengthSize += numSymbols;
- }
- }
-
- var end = 5 * histogramImage.Count;
- for (int i = 0; i < end; i++)
- {
- int bitLength = huffmanCodes[i].NumSymbols;
- huffmanCodes[i].Codes = new short[bitLength];
- huffmanCodes[i].CodeLengths = new byte[bitLength];
- if (maxNumSymbols < bitLength)
- {
- maxNumSymbols = bitLength;
- }
- }
-
- // Create Huffman trees.
- bool[] bufRle = new bool[maxNumSymbols];
- var huffTree = new HuffmanTree[3 * maxNumSymbols];
- for (int i = 0; i < huffTree.Length; i++)
- {
- huffTree[i] = new HuffmanTree();
- }
-
- for (int i = 0; i < histogramImage.Count; i++)
- {
- int codesStartIdx = 5 * i;
- Vp8LHistogram histo = histogramImage[i];
- HuffmanUtils.CreateHuffmanTree(histo.Literal, 15, bufRle, huffTree, huffmanCodes[codesStartIdx]);
- HuffmanUtils.CreateHuffmanTree(histo.Red, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 1]);
- HuffmanUtils.CreateHuffmanTree(histo.Blue, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 2]);
- HuffmanUtils.CreateHuffmanTree(histo.Alpha, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 3]);
- HuffmanUtils.CreateHuffmanTree(histo.Distance, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 4]);
- }
- }
-
- ///
- /// Computes a value that is related to the entropy created by the palette entry diff.
- ///
- /// First color.
- /// Second color.
- /// The color distance.
- [MethodImpl(InliningOptions.ShortMethod)]
- private static uint PaletteColorDistance(uint col1, uint col2)
- {
- uint diff = LosslessUtils.SubPixels(col1, col2);
- uint moreWeightForRGBThanForAlpha = 9;
- uint score = PaletteComponentDistance((diff >> 0) & 0xff);
- score += PaletteComponentDistance((diff >> 8) & 0xff);
- score += PaletteComponentDistance((diff >> 16) & 0xff);
- score *= moreWeightForRGBThanForAlpha;
- score += PaletteComponentDistance((diff >> 24) & 0xff);
-
- return score;
- }
-
- ///
- /// Calculates the huffman image bits.
- ///
- private static int GetHistoBits(int method, bool usePalette, int width, int height)
- {
- // Make tile size a function of encoding method (Range: 0 to 6).
- int histoBits = (usePalette ? 9 : 7) - method;
- while (true)
- {
- int huffImageSize = LosslessUtils.SubSampleSize(width, histoBits) * LosslessUtils.SubSampleSize(height, histoBits);
- if (huffImageSize <= WebPConstants.MaxHuffImageSize)
- {
- break;
- }
-
- histoBits++;
- }
-
- return (histoBits < WebPConstants.MinHuffmanBits) ? WebPConstants.MinHuffmanBits :
- (histoBits > WebPConstants.MaxHuffmanBits) ? WebPConstants.MaxHuffmanBits : histoBits;
- }
-
- ///
- /// Calculates the bits used for the transformation.
- ///
- [MethodImpl(InliningOptions.ShortMethod)]
- private static int GetTransformBits(int method, int histoBits)
- {
- int maxTransformBits = (method < 4) ? 6 : (method > 4) ? 4 : 5;
- int res = (histoBits > maxTransformBits) ? maxTransformBits : histoBits;
- return res;
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static Bgra32 ToBgra32(TPixel color)
- where TPixel : unmanaged, IPixel
- {
- Rgba32 rgba = default;
- color.ToRgba32(ref rgba);
- var bgra = new Bgra32(rgba.R, rgba.G, rgba.B, rgba.A);
- return bgra;
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static void AddSingle(uint p, Span a, Span r, Span g, Span b)
- {
- a[(int)(p >> 24) & 0xff]++;
- r[(int)(p >> 16) & 0xff]++;
- g[(int)(p >> 8) & 0xff]++;
- b[(int)(p >> 0) & 0xff]++;
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static void AddSingleSubGreen(uint p, Span r, Span b)
- {
- int green = (int)p >> 8; // The upper bits are masked away later.
- r[(int)((p >> 16) - green) & 0xff]++;
- b[(int)((p >> 0) - green) & 0xff]++;
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static uint ApplyPaletteHash0(uint color)
- {
- // Focus on the green color.
- return (color >> 8) & 0xff;
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static uint ApplyPaletteHash1(uint color)
- {
- // Forget about alpha.
- return ((uint)((color & 0x00ffffffu) * 4222244071ul)) >> (32 - PaletteInvSizeBits);
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static uint ApplyPaletteHash2(uint color)
- {
- // Forget about alpha.
- return ((uint)((color & 0x00ffffffu) * ((1ul << 31) - 1))) >> (32 - PaletteInvSizeBits);
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static uint HashPix(uint pix)
- {
- // Note that masking with 0xffffffffu is for preventing an
- // 'unsigned int overflow' warning. Doesn't impact the compiled code.
- return (uint)((((long)pix + (pix >> 19)) * 0x39c5fba7L) & 0xffffffffu) >> 24;
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static int PaletteCompareColorsForSort(uint p1, uint p2)
- {
- return (p1 < p2) ? -1 : 1;
- }
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private static uint PaletteComponentDistance(uint v)
- {
- return (v <= 128) ? v : (256 - v);
- }
}
}