From 7d34ceacc3c33fe7eb3c69b91d5dba9708f2ce1a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 25 Jul 2020 07:49:06 +0200 Subject: [PATCH] Split webp encoder into lossless and lossy --- .../Formats/WebP/BitWriter/Vp8LBitWriter.cs | 4 +- .../WebP/Lossless/DominantCostRange.cs | 4 +- .../Formats/WebP/Lossless/HistogramEncoder.cs | 4 +- .../Formats/WebP/Lossless/Vp8LEncoder.cs | 1363 ++++++++++++++++- .../Formats/WebP/Lossy/Vp8Encoder.cs | 46 + src/ImageSharp/Formats/WebP/WebPEncoder.cs | 15 +- .../Formats/WebP/WebPEncoderCore.cs | 1361 +--------------- 7 files changed, 1455 insertions(+), 1342 deletions(-) create mode 100644 src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs 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); - } } }