diff --git a/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs index c3cf4cb2da..4132991a7b 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs @@ -73,6 +73,18 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } } + public static void SubtractGreenFromBlueAndRed(Span pixelData, int numPixels) + { + for (int i = 0; i < numPixels; i++) + { + uint argb = pixelData[i]; + uint green = (argb >> 8) & 0xff; + uint newR = (((argb >> 16) & 0xff) - green) & 0xff; + uint newB = (((argb >> 0) & 0xff) - green) & 0xff; + pixelData[i] = (argb & 0xff00ff00u) | (newR << 16) | newB; + } + } + /// /// If there are not many unique pixel values, it is more efficient to create a color index array and replace the pixel values by the array's indices. /// This will reverse the color index transform. @@ -179,6 +191,24 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } } + public static void TransformColor(Vp8LMultipliers m, Span data, int numPixels) + { + for (int i = 0; i < numPixels; i++) + { + uint argb = data[i]; + sbyte green = U32ToS8(argb >> 8); + sbyte red = U32ToS8(argb >> 16); + int newRed = red & 0xff; + int newBlue = (int)(argb & 0xff); + newRed -= ColorTransformDelta((sbyte)m.GreenToRed, green); + newRed &= 0xff; + newBlue -= ColorTransformDelta((sbyte)m.GreenToBlue, green); + newBlue -= ColorTransformDelta((sbyte)m.RedToBlue, red); + newBlue &= 0xff; + data[i] = (uint)((argb & 0xff00ff00u) | (newRed << 16) | newBlue); + } + } + /// /// Reverses the color space transform. /// @@ -345,6 +375,86 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); } + /// + /// Bundles multiple (1, 2, 4 or 8) pixels into a single pixel. + /// + public static void BundleColorMap(Span row, int width, int xBits, Span dst) + { + int x; + if (xBits > 0) + { + int bitDepth = 1 << (3 - xBits); + int mask = (1 << xBits) - 1; + uint code = 0xff000000; + for (x = 0; x < width; x++) + { + int xsub = x & mask; + if (xsub == 0) + { + code = 0xff000000; + } + + code |= (uint)(row[x] << (8 + (bitDepth * xsub))); + dst[x >> xBits] = code; + } + } + else + { + for (x = 0; x < width; x++) + { + dst[x] = (uint)(0xff000000 | (row[x] << 8)); + } + } + } + + /// + /// Compute the combined Shanon's entropy for distribution {X} and {X+Y}. + /// + /// Shanon entropy. + public static float CombinedShannonEntropy(int[] x, int[] y) + { + double retVal = 0.0d; + uint sumX = 0, sumXY = 0; + for (int i = 0; i < 256; i++) + { + uint xi = (uint)x[i]; + if (xi != 0) + { + uint xy = xi + (uint)y[i]; + sumX += xi; + retVal -= FastSLog2(xi); + sumXY += xy; + retVal -= FastSLog2(xy); + } + else if (y[i] != 0) + { + sumXY += (uint)y[i]; + retVal -= FastSLog2((uint)y[i]); + } + } + + retVal += FastSLog2(sumX) + FastSLog2(sumXY); + return (float)retVal; + } + + public static sbyte TransformColorRed(sbyte greenToRed, uint argb) + { + sbyte green = U32ToS8(argb >> 8); + int newRed = (int)(argb >> 16); + newRed -= ColorTransformDelta(greenToRed, green); + return (sbyte)(newRed & 0xff); + } + + public static sbyte TransformColorBlue(sbyte greenToBlue, sbyte redToBlue, uint argb) + { + sbyte green = U32ToS8(argb >> 8); + sbyte red = U32ToS8(argb >> 16); + int newBlue = (int)(argb & 0xff); + newBlue -= ColorTransformDelta(greenToBlue, green); + newBlue -= ColorTransformDelta(redToBlue, red); + return (sbyte)(newBlue & 0xff); + } + /// /// Fast calculation of log2(v) for integer input. /// @@ -362,6 +472,26 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless return (v < LogLookupIdxMax) ? WebPLookupTables.SLog2Table[v] : FastSLog2Slow(v); } + [MethodImpl(InliningOptions.ShortMethod)] + public static void ColorCodeToMultipliers(uint colorCode, ref Vp8LMultipliers m) + { + m.GreenToRed = (byte)(colorCode & 0xff); + m.GreenToBlue = (byte)((colorCode >> 8) & 0xff); + m.RedToBlue = (byte)((colorCode >> 16) & 0xff); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int NearLosslessBits(int nearLosslessQuality) + { + // 100 -> 0 + // 80..99 -> 1 + // 60..79 -> 2 + // 40..59 -> 3 + // 20..39 -> 4 + // 0..19 -> 5 + return 5 - (nearLosslessQuality / 20); + } + private static float FastSLog2Slow(uint v) { Guard.MustBeGreaterThanOrEqualTo(v, LogLookupIdxMax, nameof(v)); @@ -605,86 +735,224 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor2(Span top, int idx) + public static uint Predictor2(Span top, int idx) { return top[idx]; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor3(Span top, int idx) + public static uint Predictor3(Span top, int idx) { return top[idx + 1]; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor4(Span top, int idx) + public static uint Predictor4(Span top, int idx) { return top[idx - 1]; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor5(uint left, Span top, int idx) + public static uint Predictor5(uint left, Span top, int idx) { uint pred = Average3(left, top[idx], top[idx + 1]); return pred; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor6(uint left, Span top, int idx) + public static uint Predictor6(uint left, Span top, int idx) { uint pred = Average2(left, top[idx - 1]); return pred; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor7(uint left, Span top, int idx) + public static uint Predictor7(uint left, Span top, int idx) { uint pred = Average2(left, top[idx]); return pred; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor8(Span top, int idx) + public static uint Predictor8(Span top, int idx) { uint pred = Average2(top[idx - 1], top[idx]); return pred; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor9(Span top, int idx) + public static uint Predictor9(Span top, int idx) { uint pred = Average2(top[idx], top[idx + 1]); return pred; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor10(uint left, Span top, int idx) + public static uint Predictor10(uint left, Span top, int idx) { uint pred = Average4(left, top[idx - 1], top[idx], top[idx + 1]); return pred; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor11(uint left, Span top, int idx) + public static uint Predictor11(uint left, Span top, int idx) { uint pred = Select(top[idx], left, top[idx - 1]); return pred; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor12(uint left, Span top, int idx) + public static uint Predictor12(uint left, Span top, int idx) { uint pred = ClampedAddSubtractFull(left, top[idx], top[idx - 1]); return pred; } [MethodImpl(InliningOptions.ShortMethod)] - private static uint Predictor13(uint left, Span top, int idx) + public static uint Predictor13(uint left, Span top, int idx) { uint pred = ClampedAddSubtractHalf(left, top[idx], top[idx - 1]); return pred; } + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub0(Span input, int numPixels, Span output) + { + for (int i = 0; i < numPixels; i++) + { + output[i] = SubPixels(input[i], WebPConstants.ArgbBlack); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub1(Span input, int idx, int numPixels, Span output) + { + for (int i = 0; i < numPixels; i++) + { + output[i] = SubPixels(input[idx + i], input[idx + i - 1]); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub2(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor2(upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub3(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor3(upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub4(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor4(upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub5(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor5(input[idx - 1], upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub6(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor6(input[idx - 1], upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub7(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor7(input[idx - 1], upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub8(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor8(upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub9(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor9(upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub10(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor10(input[idx - 1], upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub11(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor11(input[idx - 1], upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub12(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor12(input[idx - 1], upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub13(Span input, int idx, Span upper, int numPixels, Span output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor13(input[idx - 1], upper, x); + output[x] = SubPixels(input[idx + x], pred); + } + } + private static uint ClampedAddSubtractFull(uint c0, uint c1, uint c2) { int a = AddSubtractComponentFull( @@ -780,7 +1048,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless /// Sum of each component, mod 256. /// [MethodImpl(InliningOptions.ShortMethod)] - private static uint AddPixels(uint a, uint b) + public static uint AddPixels(uint a, uint b) { uint alphaAndGreen = (a & 0xff00ff00u) + (b & 0xff00ff00u); uint redAndBlue = (a & 0x00ff00ffu) + (b & 0x00ff00ffu); @@ -800,20 +1068,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } [MethodImpl(InliningOptions.ShortMethod)] - private static void ColorCodeToMultipliers(uint colorCode, ref Vp8LMultipliers m) + private static sbyte U32ToS8(uint v) { - m.GreenToRed = (byte)(colorCode & 0xff); - m.GreenToBlue = (byte)((colorCode >> 8) & 0xff); - m.RedToBlue = (byte)((colorCode >> 16) & 0xff); - } - - internal struct Vp8LMultipliers - { - public byte GreenToRed; - - public byte GreenToBlue; - - public byte RedToBlue; + return (sbyte)(v & 0xff); } } } diff --git a/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs new file mode 100644 index 0000000000..c954e18b79 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs @@ -0,0 +1,908 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the GNU Affero General Public License, Version 3. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.WebP.Lossless +{ + /// + /// Image transform methods for the lossless webp encoder. + /// + internal static class PredictorEncoder + { + private const int GreenRedToBlueNumAxis = 8; + + private const int GreenRedToBlueMaxIters = 7; + + private const float MaxDiffCost = 1e30f; + + private const uint MaskAlpha = 0xff000000; + + private const float SpatialPredictorBias = 15.0f; + + /// + /// Finds the best predictor for each tile, and converts the image to residuals + /// with respect to predictions. If nearLosslessQuality < 100, applies + /// near lossless processing, shaving off more bits of residuals for lower qualities. + /// + public static void ResidualImage(int width, int height, int bits, Span argb, Span argbScratch, Span image, int nearLosslessQuality, bool exact, bool usedSubtractGreen) + { + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + int tilesPerCol = LosslessUtils.SubSampleSize(height, bits); + int maxQuantization = 1 << LosslessUtils.NearLosslessBits(nearLosslessQuality); + int[][] histo = new int[4][]; + for (int i = 0; i < 4; i++) + { + histo[i] = new int[256]; + } + + for (int tileY = 0; tileY < tilesPerCol; tileY++) + { + for (int tileX = 0; tileX < tilesPerRow; tileX++) + { + int pred = GetBestPredictorForTile(width, height, tileX, tileY, bits, histo, argbScratch, argb, maxQuantization, exact, usedSubtractGreen, image); + image[(tileY * tilesPerRow) + tileX] = (uint)(WebPConstants.ArgbBlack | (pred << 8)); + } + } + + CopyImageWithPrediction(width, height, bits, image, argbScratch, argb, maxQuantization, exact, usedSubtractGreen); + } + + public static void ColorSpaceTransform(int width, int height, int bits, int quality, Span argb, Span image) + { + int maxTileSize = 1 << bits; + int tileXSize = LosslessUtils.SubSampleSize(width, bits); + int tileYSize = LosslessUtils.SubSampleSize(height, bits); + int[] accumulatedRedHisto = new int[256]; + int[] accumulatedBlueHisto = new int[256]; + var prevX = default(Vp8LMultipliers); + var prevY = default(Vp8LMultipliers); + for (int tileY = 0; tileY < tileYSize; tileY++) + { + for (int tileX = 0; tileX < tileXSize; tileX++) + { + int tileXOffset = tileX * maxTileSize; + int tileYOffset = tileY * maxTileSize; + int allXMax = GetMin(tileXOffset + maxTileSize, width); + int allYMax = GetMin(tileYOffset + maxTileSize, height); + int offset = (tileY * tileXSize) + tileX; + if (tileY != 0) + { + LosslessUtils.ColorCodeToMultipliers(image[offset - tileXSize], ref prevY); + } + + prevX = GetBestColorTransformForTile(tileX, tileY, bits, + prevX, prevY, + quality, width, height, + accumulatedRedHisto, + accumulatedBlueHisto, + argb); + + image[offset] = MultipliersToColorCode(prevX); + CopyTileWithColorTransform(width, height, tileXOffset, tileYOffset, maxTileSize, prevX, argb); + + // Gather accumulated histogram data. + for (int y = tileYOffset; y < allYMax; y++) + { + int ix = (y * width) + tileXOffset; + int ixEnd = ix + allXMax - tileXOffset; + + for (; ix < ixEnd; ix++) + { + uint pix = argb[ix]; + if (ix >= 2 && pix == argb[ix - 2] && pix == argb[ix - 1]) + { + continue; // Repeated pixels are handled by backward references. + } + + if (ix >= width + 2 && argb[ix - 2] == argb[ix - width - 2] && argb[ix - 1] == argb[ix - width - 1] && pix == argb[ix - width]) + { + continue; // Repeated pixels are handled by backward references. + } + + accumulatedRedHisto[(pix >> 16) & 0xff]++; + accumulatedBlueHisto[(pix >> 0) & 0xff]++; + } + } + } + } + } + + /// + /// Returns best predictor and updates the accumulated histogram. + /// If max_quantization > 1, assumes that near lossless processing will be + /// applied, quantizing residuals to multiples of quantization levels up to + /// maxQuantization (the actual quantization level depends on smoothness near + /// the given pixel). + /// + /// Best predictor. + private static int GetBestPredictorForTile(int width, int height, int tileX, int tileY, + int bits, int[][] accumulated, Span argbScratch, Span argb, + int maxQuantization, bool exact, bool usedSubtractGreen, Span modes) + { + const int numPredModes = 14; + int startX = tileX << bits; + int startY = tileY << bits; + int tileSize = 1 << bits; + int maxY = GetMin(tileSize, height - startY); + int maxX = GetMin(tileSize, width - startX); + + // Whether there exist columns just outside the tile. + int haveLeft = (startX > 0) ? 1 : 0; + + // Position and size of the strip covering the tile and adjacent columns if they exist. + int contextStartX = startX - haveLeft; + int contextWidth = maxX + haveLeft + (maxX < width ? 1 : 0) - startX; + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + + // Prediction modes of the left and above neighbor tiles. + int leftMode = (int)((tileX > 0) ? (modes[(tileY * tilesPerRow) + tileX - 1] >> 8) & 0xff : 0xff); + int aboveMode = (int)((tileY > 0) ? (modes[((tileY - 1) * tilesPerRow) + tileX] >> 8) & 0xff : 0xff); + + // The width of upper_row and current_row is one pixel larger than image width + // to allow the top right pixel to point to the leftmost pixel of the next row + // when at the right edge. + Span upperRow = argbScratch; + Span currentRow = upperRow.Slice(width + 1); + Span maxDiffs = MemoryMarshal.Cast(currentRow.Slice(width + 1)); + float bestDiff = MaxDiffCost; + int bestMode = 0; + uint[] residuals = new uint[1 << WebPConstants.MaxTransformBits]; + int[][] histoArgb = new int[4][]; + int[][] bestHisto = new int[4][]; + for (int i = 0; i < 4; i++) + { + histoArgb[i] = new int[256]; + bestHisto[i] = new int[256]; + } + + for (int mode = 0; mode < numPredModes; mode++) + { + float curDiff; + for (int i = 0; i < 4; i++) + { + histoArgb[i].AsSpan().Fill(0); + } + + if (startY > 0) + { + // Read the row above the tile which will become the first upper_row. + // Include a pixel to the left if it exists; include a pixel to the right + // in all cases (wrapping to the leftmost pixel of the next row if it does + // not exist). + Span src = argb.Slice(((startY - 1) * width) + contextStartX, maxX + haveLeft + 1); + Span dst = currentRow.Slice(contextStartX); + src.CopyTo(dst); + } + + for (int relativeY = 0; relativeY < maxY; relativeY++) + { + int y = startY + relativeY; + Span tmp = upperRow; + upperRow = currentRow; + currentRow = tmp; + + // Read current_row. Include a pixel to the left if it exists; include a + // pixel to the right in all cases except at the bottom right corner of + // the image (wrapping to the leftmost pixel of the next row if it does + // not exist in the current row). + Span src = argb.Slice((y * width) + contextStartX, maxX + haveLeft + ((y + 1) < height ? 1 : 0)); + Span dst = currentRow.Slice(contextStartX); + src.CopyTo(dst); + if (maxQuantization > 1 && y >= 1 && y + 1 < height) + { + MaxDiffsForRow(contextWidth, width, argb.Slice((y * width) + contextStartX), maxDiffs.Slice(contextStartX), usedSubtractGreen); + } + + GetResidual(width, height, upperRow, currentRow, maxDiffs, mode, startX, startX + maxX, y, maxQuantization, exact, usedSubtractGreen, residuals); + for (int relativeX = 0; relativeX < maxX; relativeX++) + { + UpdateHisto(histoArgb, residuals[relativeX]); + } + } + + curDiff = PredictionCostSpatialHistogram(accumulated, histoArgb); + + // Favor keeping the areas locally similar. + if (mode == leftMode) + { + curDiff -= SpatialPredictorBias; + } + + if (mode == aboveMode) + { + curDiff -= SpatialPredictorBias; + } + + if (curDiff < bestDiff) + { + for (int i = 0; i < 4; i++) + { + histoArgb[i].AsSpan().CopyTo(bestHisto[i]); + } + + bestDiff = curDiff; + bestMode = mode; + } + } + + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 256; j++) + { + accumulated[i][j] += bestHisto[i][j]; + } + } + + return bestMode; + } + + /// + /// Stores the difference between the pixel and its prediction in "output". + /// In case of a lossy encoding, updates the source image to avoid propagating + /// the deviation further to pixels which depend on the current pixel for their + /// predictions. + /// + private static void GetResidual(int width, int height, Span upperRow, Span currentRow, Span maxDiffs, int mode, int xStart, int xEnd, int y, int maxQuantization, bool exact, bool usedSubtractGreen, Span output) + { + if (exact) + { + PredictBatch(mode, xStart, y, xEnd - xStart, currentRow, upperRow, output); + } + else + { + for (int x = xStart; x < xEnd; x++) + { + uint predict = 0; + uint residual; + if (y == 0) + { + predict = (x == 0) ? WebPConstants.ArgbBlack : currentRow[x - 1]; // Left. + } + else if (x == 0) + { + predict = upperRow[x]; // Top. + } + else + { + switch (mode) + { + case 0: + predict = WebPConstants.ArgbBlack; + break; + case 1: + predict = currentRow[x - 1]; + break; + case 2: + predict = LosslessUtils.Predictor2(upperRow, x); + break; + case 3: + predict = LosslessUtils.Predictor3(upperRow, x); + break; + case 4: + predict = LosslessUtils.Predictor4(upperRow, x); + break; + case 5: + predict = LosslessUtils.Predictor5(currentRow[x - 1], upperRow, x); + break; + case 6: + predict = LosslessUtils.Predictor6(currentRow[x - 1], upperRow, x); + break; + case 7: + predict = LosslessUtils.Predictor7(currentRow[x - 1], upperRow, x); + break; + case 8: + predict = LosslessUtils.Predictor8(upperRow, x); + break; + case 9: + predict = LosslessUtils.Predictor9(upperRow, x); + break; + case 10: + predict = LosslessUtils.Predictor10(currentRow[x - 1], upperRow, x); + break; + case 11: + predict = LosslessUtils.Predictor11(currentRow[x - 1], upperRow, x); + break; + case 12: + predict = LosslessUtils.Predictor12(currentRow[x - 1], upperRow, x); + break; + case 13: + predict = LosslessUtils.Predictor13(currentRow[x - 1], upperRow, x); + break; + } + } + + if (maxQuantization == 1 || mode == 0 || y == 0 || y == height - 1 || x == 0 || x == width - 1) + { + residual = LosslessUtils.SubPixels(currentRow[x], predict); + } + else + { + residual = NearLossless(currentRow[x], predict, maxQuantization, maxDiffs[x], usedSubtractGreen); + + // Update the source image. + currentRow[x] = LosslessUtils.AddPixels(predict, residual); + + // x is never 0 here so we do not need to update upper_row like below. + } + + if ((currentRow[x] & MaskAlpha) == 0) + { + // If alpha is 0, cleanup RGB. We can choose the RGB values of the + // residual for best compression. The prediction of alpha itself can be + // non-zero and must be kept though. We choose RGB of the residual to be + // 0. + residual &= MaskAlpha; + + // Update the source image. + currentRow[x] = predict & ~MaskAlpha; + + // The prediction for the rightmost pixel in a row uses the leftmost + // pixel + // in that row as its top-right context pixel. Hence if we change the + // leftmost pixel of current_row, the corresponding change must be + // applied + // to upper_row as well where top-right context is being read from. + if (x == 0 && y != 0) + { + upperRow[width] = currentRow[0]; + } + } + + output[x - xStart] = residual; + } + } + } + + /// + /// Quantize every component of the difference between the actual pixel value and + /// its prediction to a multiple of a quantization (a power of 2, not larger than + /// maxQuantization which is a power of 2, smaller than maxDiff). Take care if + /// value and predict have undergone subtract green, which means that red and + /// blue are represented as offsets from green. + /// + private static uint NearLossless(uint value, uint predict, int maxQuantization, int maxDiff, bool usedSubtractGreen) + { + int quantization; + byte newGreen = 0; + byte greenDiff = 0; + byte a, r, g, b; + if (maxDiff <= 2) + { + return LosslessUtils.SubPixels(value, predict); + } + + quantization = maxQuantization; + while (quantization >= maxDiff) + { + quantization >>= 1; + } + + if ((value >> 24) == 0 || (value >> 24) == 0xff) + { + // Preserve transparency of fully transparent or fully opaque pixels. + a = NearLosslessDiff((byte)((value >> 24) & 0xff), (byte)((predict >> 24) & 0xff)); + } + else + { + a = NearLosslessComponent((byte)(value >> 24), (byte)(predict >> 24), 0xff, quantization); + } + + g = NearLosslessComponent((byte)((value >> 8) & 0xff), (byte)((predict >> 8) & 0xff), 0xff, quantization); + + if (usedSubtractGreen) + { + // The green offset will be added to red and blue components during decoding + // to obtain the actual red and blue values. + newGreen = (byte)(((predict >> 8) + g) & 0xff); + + // The amount by which green has been adjusted during quantization. It is + // subtracted from red and blue for compensation, to avoid accumulating two + // quantization errors in them. + greenDiff = NearLosslessDiff(newGreen, (byte)((value >> 8) & 0xff)); + } + + r = NearLosslessComponent(NearLosslessDiff((byte)((value >> 16) & 0xff), greenDiff), (byte)((predict >> 16) & 0xff), (byte)(0xff - newGreen), quantization); + b = NearLosslessComponent(NearLosslessDiff((byte)(value & 0xff), greenDiff), (byte)(predict & 0xff), (byte)(0xff - newGreen), quantization); + + return ((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | b; + } + + /// + /// Quantize the difference between the actual component value and its prediction + /// to a multiple of quantization, working modulo 256, taking care not to cross + /// a boundary (inclusive upper limit). + /// + private static byte NearLosslessComponent(byte value, byte predict, byte boundary, int quantization) + { + int residual = (value - predict) & 0xff; + int boundaryResidual = (boundary - predict) & 0xff; + int lower = residual & ~(quantization - 1); + int upper = lower + quantization; + + // Resolve ties towards a value closer to the prediction (i.e. towards lower + // if value comes after prediction and towards upper otherwise). + int bias = ((boundary - value) & 0xff) < boundaryResidual ? 1 : 0; + + if (residual - lower < upper - residual + bias) + { + // lower is closer to residual than upper. + if (residual > boundaryResidual && lower <= boundaryResidual) + { + // Halve quantization step to avoid crossing boundary. This midpoint is + // on the same side of boundary as residual because midpoint >= residual + // (since lower is closer than upper) and residual is above the boundary. + return (byte)(lower + (quantization >> 1)); + } + + return (byte)lower; + } + else + { + // upper is closer to residual than lower. + if (residual <= boundaryResidual && upper > boundaryResidual) + { + // Halve quantization step to avoid crossing boundary. This midpoint is + // on the same side of boundary as residual because midpoint <= residual + // (since upper is closer than lower) and residual is below the boundary. + return (byte)(lower + (quantization >> 1)); + } + + return (byte)(upper & 0xff); + } + } + + /// + /// Converts pixels of the image to residuals with respect to predictions. + /// If max_quantization > 1, applies near lossless processing, quantizing + /// residuals to multiples of quantization levels up to max_quantization + /// (the actual quantization level depends on smoothness near the given pixel). + /// + private static void CopyImageWithPrediction(int width, int height, int bits, Span modes, Span argbScratch, Span argb, int maxQuantization, bool exact, bool usedSubtractGreen) + { + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + + // The width of upper_row and current_row is one pixel larger than image width + // to allow the top right pixel to point to the leftmost pixel of the next row + // when at the right edge. + Span upperRow = argbScratch; + Span currentRow = upperRow.Slice(width + 1); + Span currentMaxDiffs = MemoryMarshal.Cast(currentRow.Slice(width + 1)); + Span lowerMaxDiffs = currentMaxDiffs.Slice(width); + for (int y = 0; y < height; y++) + { + Span tmp32 = upperRow; + upperRow = currentRow; + currentRow = tmp32; + argb.Slice(y * width, width + y + (1 < height ? 1 : 0)).CopyTo(currentRow); + if (maxQuantization > 1) + { + // Compute max_diffs for the lower row now, because that needs the + // contents of argb for the current row, which we will overwrite with + // residuals before proceeding with the next row. + Span tmp8 = currentMaxDiffs; + currentMaxDiffs = lowerMaxDiffs; + lowerMaxDiffs = tmp8; + if (y + 2 < height) + { + MaxDiffsForRow(width, width, argb.Slice((y + 1) * width), lowerMaxDiffs, usedSubtractGreen); + } + } + + for (int x = 0; x < width;) + { + int mode = (int)((modes[((y >> bits) * tilesPerRow) + (x >> bits)] >> 8) & 0xff); + int xEnd = x + (1 << bits); + if (xEnd > width) + { + xEnd = width; + } + + GetResidual(width, height, upperRow, currentRow, currentMaxDiffs, + mode, x, xEnd, y, maxQuantization, exact, + usedSubtractGreen, argb.Slice((y * width) + x)); + x = xEnd; + } + } + } + + private static void PredictBatch(int mode, int xStart, int y, int numPixels, Span current, Span upper, Span output) + { + if (xStart == 0) + { + if (y == 0) + { + // ARGB_BLACK. + LosslessUtils.PredictorSub0(current, 1, output); + } + else + { + // Top one. + LosslessUtils.PredictorSub2(current, 0, upper, 1, output); + } + + xStart++; + output = output.Slice(1); + numPixels--; + } + + if (y == 0) + { + // Left one. + LosslessUtils.PredictorSub1(current, xStart, numPixels, output); + } + else + { + switch (mode) + { + case 0: + LosslessUtils.PredictorSub0(current, numPixels, output); + break; + case 1: + LosslessUtils.PredictorSub1(current, xStart, numPixels, output); + break; + case 2: + LosslessUtils.PredictorSub2(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 3: + LosslessUtils.PredictorSub3(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 4: + LosslessUtils.PredictorSub4(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 5: + LosslessUtils.PredictorSub5(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 6: + LosslessUtils.PredictorSub6(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 7: + LosslessUtils.PredictorSub7(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 8: + LosslessUtils.PredictorSub8(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 9: + LosslessUtils.PredictorSub9(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 10: + LosslessUtils.PredictorSub10(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 11: + LosslessUtils.PredictorSub11(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 12: + LosslessUtils.PredictorSub12(current, xStart, upper.Slice(xStart), numPixels, output); + break; + case 13: + LosslessUtils.PredictorSub13(current, xStart, upper.Slice(xStart), numPixels, output); + break; + } + } + } + + private static void MaxDiffsForRow(int width, int stride, Span argb, Span maxDiffs, bool usedSubtractGreen) + { + if (width <= 2) + { + return; + } + + uint current = argb[0]; + uint right = argb[1]; + if (usedSubtractGreen) + { + current = AddGreenToBlueAndRed(current); + right = AddGreenToBlueAndRed(right); + } + + for (int x = 1; x < width - 1; x++) + { + uint up = argb[-stride + x]; // TODO: -stride! + uint down = argb[stride + x]; + uint left = current; + current = right; + right = argb[x + 1]; + if (usedSubtractGreen) + { + up = AddGreenToBlueAndRed(up); + down = AddGreenToBlueAndRed(down); + right = AddGreenToBlueAndRed(right); + } + + maxDiffs[x] = (byte)MaxDiffAroundPixel(current, up, down, left, right); + } + } + + private static int MaxDiffBetweenPixels(uint p1, uint p2) + { + int diffA = Math.Abs((int)(p1 >> 24) - (int)(p2 >> 24)); + int diffR = Math.Abs((int)((p1 >> 16) & 0xff) - (int)((p2 >> 16) & 0xff)); + int diffG = Math.Abs((int)((p1 >> 8) & 0xff) - (int)((p2 >> 8) & 0xff)); + int diffB = Math.Abs((int)(p1 & 0xff) - (int)(p2 & 0xff)); + return GetMax(GetMax(diffA, diffR), GetMax(diffG, diffB)); + } + + private static int MaxDiffAroundPixel(uint current, uint up, uint down, uint left, uint right) + { + int diffUp = MaxDiffBetweenPixels(current, up); + int diffDown = MaxDiffBetweenPixels(current, down); + int diffLeft = MaxDiffBetweenPixels(current, left); + int diffRight = MaxDiffBetweenPixels(current, right); + return GetMax(GetMax(diffUp, diffDown), GetMax(diffLeft, diffRight)); + } + + private static void UpdateHisto(int[][] histoArgb, uint argb) + { + ++histoArgb[0][argb >> 24]; + ++histoArgb[1][(argb >> 16) & 0xff]; + ++histoArgb[2][(argb >> 8) & 0xff]; + ++histoArgb[3][argb & 0xff]; + } + + private static uint AddGreenToBlueAndRed(uint argb) + { + uint green = (argb >> 8) & 0xff; + uint redBlue = argb & 0x00ff00ffu; + redBlue += (green << 16) | green; + redBlue &= 0x00ff00ffu; + return (argb & 0xff00ff00u) | redBlue; + } + + private static void CopyTileWithColorTransform(int xSize, int ySize, int tileX, int tileY, int maxTileSize, Vp8LMultipliers colorTransform, Span argb) + { + int xScan = GetMin(maxTileSize, xSize - tileX); + int yScan = GetMin(maxTileSize, ySize - tileY); + argb = argb.Slice((tileY * xSize) + tileX); + while (yScan-- > 0) + { + LosslessUtils.TransformColor(colorTransform, argb, xScan); + argb = argb.Slice(xSize); + } + } + + private static Vp8LMultipliers GetBestColorTransformForTile(int tile_x, int tile_y, int bits, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int xSize, int ySize, int[] accumulatedRedHisto, int[] accumulatedBlueHisto, Span argb) + { + int maxTileSize = 1 << bits; + int tileYOffset = tile_y * maxTileSize; + int tileXOffset = tile_x * maxTileSize; + int allXMax = GetMin(tileXOffset + maxTileSize, xSize); + int allYMax = GetMin(tileYOffset + maxTileSize, ySize); + int tileWidth = allXMax - tileXOffset; + int tileHeight = allYMax - tileYOffset; + Span tileArgb = argb.Slice((tileYOffset * xSize) + tileXOffset); + + var bestTx = default(Vp8LMultipliers); + + GetBestGreenToRed(tileArgb, xSize, tileWidth, tileHeight, prevX, prevY, quality, accumulatedRedHisto, ref bestTx); + + GetBestGreenRedToBlue(tileArgb, xSize, tileWidth, tileHeight, prevX, prevY, quality, accumulatedBlueHisto, ref bestTx); + + return bestTx; + } + + private static void GetBestGreenToRed(Span argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int[] accumulatedRedHisto, ref Vp8LMultipliers bestTx) + { + int maxIters = 4 + ((7 * quality) >> 8); // in range [4..6] + int greenToRedBest = 0; + float bestDiff = GetPredictionCostCrossColorRed(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToRedBest, accumulatedRedHisto); + for (int iter = 0; iter < maxIters; iter++) + { + // ColorTransformDelta is a 3.5 bit fixed point, so 32 is equal to + // one in color computation. Having initial delta here as 1 is sufficient + // to explore the range of (-2, 2). + int delta = 32 >> iter; + + // Try a negative and a positive delta from the best known value. + for (int offset = -delta; offset <= delta; offset += 2 * delta) + { + int greenToRedCur = offset + greenToRedBest; + float curDiff = GetPredictionCostCrossColorRed(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToRedCur, accumulatedRedHisto); + if (curDiff < bestDiff) + { + bestDiff = curDiff; + greenToRedBest = greenToRedCur; + } + } + } + + bestTx.GreenToRed = (byte)(greenToRedBest & 0xff); + } + + private static void GetBestGreenRedToBlue(Span argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int[] accumulatedBlueHisto, ref Vp8LMultipliers bestTx) + { + int iters = (quality < 25) ? 1 : (quality > 50) ? GreenRedToBlueMaxIters : 4; + int greenToBlueBest = 0; + int redToBlueBest = 0; + sbyte[][] offset = { new sbyte[] { 0, -1 }, new sbyte[] { 0, 1 }, new sbyte[] { -1, 0 }, new sbyte[] { 1, 0 }, new sbyte[] { -1, -1 }, new sbyte[] { -1, 1 }, new sbyte[] { 1, -1 }, new sbyte[] { 1, 1 } }; + sbyte[] deltaLut = { 16, 16, 8, 4, 2, 2, 2 }; + + // Initial value at origin: + float bestDiff = GetPredictionCostCrossColorBlue(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToBlueBest, redToBlueBest, accumulatedBlueHisto); + for (int iter = 0; iter < iters; iter++) + { + int delta = deltaLut[iter]; + for (int axis = 0; axis < GreenRedToBlueNumAxis; axis++) + { + int greenToBlueCur = (offset[axis][0] * delta) + greenToBlueBest; + int redToBlueCur = (offset[axis][1] * delta) + redToBlueBest; + float curDiff = GetPredictionCostCrossColorBlue(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToBlueCur, redToBlueCur, accumulatedBlueHisto); + if (curDiff < bestDiff) + { + bestDiff = curDiff; + greenToBlueBest = greenToBlueCur; + redToBlueBest = redToBlueCur; + } + + if (quality < 25 && iter == 4) + { + // Only axis aligned diffs for lower quality. + break; // next iter. + } + } + + if (delta == 2 && greenToBlueBest == 0 && redToBlueBest == 0) + { + // Further iterations would not help. + break; // out of iter-loop. + } + } + + bestTx.GreenToBlue = (byte)(greenToBlueBest & 0xff); + bestTx.RedToBlue = (byte)(redToBlueBest & 0xff); + } + + private static float GetPredictionCostCrossColorRed(Span argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int greenToRed, int[] accumulatedRedHisto) + { + int[] histo = new int[256]; + + CollectColorRedTransforms(argb, stride, tileWidth, tileHeight, greenToRed, histo); + float curDiff = PredictionCostCrossColor(accumulatedRedHisto, histo); + + if ((byte)greenToRed == prevX.GreenToRed) + { + curDiff -= 3; // Favor keeping the areas locally similar. + } + + if ((byte)greenToRed == prevY.GreenToRed) + { + curDiff -= 3; // Favor keeping the areas locally similar. + } + + if (greenToRed == 0) + { + curDiff -= 3; + } + + return curDiff; + } + + private static float GetPredictionCostCrossColorBlue(Span argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int greenToBlue, int redToBlue, int[] accumulatedBlueHisto) + { + int[] histo = new int[256]; + + CollectColorBlueTransforms(argb, stride, tileWidth, tileHeight, greenToBlue, redToBlue, histo); + float curDiff = PredictionCostCrossColor(accumulatedBlueHisto, histo); + if ((byte)greenToBlue == prevX.GreenToBlue) + { + curDiff -= 3; // Favor keeping the areas locally similar. + } + + if ((byte)greenToBlue == prevY.GreenToBlue) + { + curDiff -= 3; // Favor keeping the areas locally similar. + } + + if ((byte)redToBlue == prevX.RedToBlue) + { + curDiff -= 3; // Favor keeping the areas locally similar. + } + + if ((byte)redToBlue == prevY.RedToBlue) + { + curDiff -= 3; // Favor keeping the areas locally similar. + } + + if (greenToBlue == 0) + { + curDiff -= 3; + } + + if (redToBlue == 0) + { + curDiff -= 3; + } + + return curDiff; + } + + private static void CollectColorRedTransforms(Span argb, int stride, int tileWidth, int tileHeight, int greenToRed, int[] histo) + { + int pos = 0; + while (tileHeight-- > 0) + { + for (int x = 0; x < tileWidth; x++) + { + ++histo[LosslessUtils.TransformColorRed((sbyte)greenToRed, argb[pos + x])]; + } + + pos += stride; + } + } + + private static void CollectColorBlueTransforms(Span argb, int stride, int tileWidth, int tileHeight, int greenToBlue, int redToBlue, int[] histo) + { + int pos = 0; + while (tileHeight-- > 0) + { + for (int x = 0; x < tileWidth; x++) + { + ++histo[LosslessUtils.TransformColorBlue((sbyte)greenToBlue, (sbyte)redToBlue, argb[pos + x])]; + } + + pos += stride; + } + } + + private static float PredictionCostSpatialHistogram(int[][] accumulated, int[][] tile) + { + double retVal = 0.0d; + for (int i = 0; i < 4; i++) + { + double kExpValue = 0.94; + retVal += PredictionCostSpatial(tile[i], 1, kExpValue); + retVal += LosslessUtils.CombinedShannonEntropy(tile[i], accumulated[i]); + } + + return (float)retVal; + } + + private static float PredictionCostCrossColor(int[] accumulated, int[] counts) + { + // Favor low entropy, locally and globally. + // Favor small absolute values for PredictionCostSpatial. + const double expValue = 2.4d; + return LosslessUtils.CombinedShannonEntropy(counts, accumulated) + PredictionCostSpatial(counts, 3, expValue); + } + + private static float PredictionCostSpatial(int[] counts, int weight0, double expVal) + { + int significantSymbols = 256 >> 4; + double expDecayFactor = 0.6; + double bits = weight0 * counts[0]; + for (int i = 1; i < significantSymbols; i++) + { + bits += expVal * (counts[i] + counts[256 - i]); + expVal *= expDecayFactor; + } + + return (float)(-0.1 * bits); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte NearLosslessDiff(byte a, byte b) + { + return (byte)((a - b) & 0xff); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint MultipliersToColorCode(Vp8LMultipliers m) + { + return 0xff000000u | ((uint)m.RedToBlue << 16) | ((uint)m.GreenToBlue << 8) | m.GreenToRed; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetMin(int a, int b) + { + return (a > b) ? b : a; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetMax(int a, int b) + { + return (a < b) ? b : a; + } + } +} diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs index 7df49bac3e..0f54285a54 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs @@ -22,15 +22,25 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless /// private const int MinBlockSize = 256; + private MemoryAllocator memoryAllocator; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The width of the input image. + /// The height of the input image. public Vp8LEncoder(MemoryAllocator memoryAllocator, int width, int height) { var pixelCount = width * height; + this.Bgra = memoryAllocator.Allocate(pixelCount); this.Palette = memoryAllocator.Allocate(WebPConstants.MaxPaletteSize); this.Refs = new Vp8LBackwardRefs[3]; this.HashChain = new Vp8LHashChain(pixelCount); + this.memoryAllocator = memoryAllocator; - // We round the block size up, so we're guaranteed to have at most MAX_REFS_BLOCK_PER_IMAGE blocks used: + // We round the block size up, so we're guaranteed to have at most MaxRefsBlockPerImage blocks used: int refsBlockSize = ((pixelCount - 1) / MaxRefsBlockPerImage) + 1; for (int i = 0; i < this.Refs.Length; ++i) { @@ -39,6 +49,21 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } } + /// + /// Gets transformed image data. + /// + public IMemoryOwner Bgra { get; } + + /// + /// Gets the scratch memory for bgra rows used for prediction. + /// + public IMemoryOwner BgraScratch { get; set; } + + /// + /// Gets or sets the packed image width. + /// + public int CurrentWidth { get; set; } + /// /// Gets or sets the huffman image bits. /// @@ -50,9 +75,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless public int TransformBits { get; set; } /// - /// Gets or sets a value indicating whether to use a color cache. + /// Gets or sets the transform data. /// - public bool UseColorCache { get; set; } + public IMemoryOwner TransformData { get; set; } + + /// + /// Gets or sets the cache bits. If equal to 0, don't use color cache. + /// + public int CacheBits { get; set; } /// /// Gets or sets a value indicating whether to use the cross color transform. @@ -84,14 +114,36 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless /// public IMemoryOwner Palette { get; } + /// + /// Gets the backward references. + /// public Vp8LBackwardRefs[] Refs { get; } + /// + /// Gets the hash chain. + /// public Vp8LHashChain HashChain { get; } + public void AllocateTransformBuffer(int width, int height) + { + int imageSize = width * height; + + // VP8LResidualImage needs room for 2 scanlines of uint32 pixels with an extra + // pixel in each, plus 2 regular scanlines of bytes. + int argbScratchSize = this.UsePredictorTransform ? ((width + 1) * 2) + (((width * 2) + 4 - 1) / 4) : 0; + int transformDataSize = (this.UsePredictorTransform || this.UseCrossColorTransform) ? LosslessUtils.SubSampleSize(width, this.TransformBits) * LosslessUtils.SubSampleSize(height, this.TransformBits) : 0; + + this.BgraScratch = this.memoryAllocator.Allocate(argbScratchSize); + this.TransformData = this.memoryAllocator.Allocate(transformDataSize); + } + /// public void Dispose() { + this.Bgra.Dispose(); + this.BgraScratch.Dispose(); this.Palette.Dispose(); + this.TransformData.Dispose(); } } } diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LMultipliers.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LMultipliers.cs new file mode 100644 index 0000000000..3ce9a93ecb --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LMultipliers.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the GNU Affero General Public License, Version 3. + +namespace SixLabors.ImageSharp.Formats.WebP.Lossless +{ + internal struct Vp8LMultipliers + { + public byte GreenToRed; + + public byte GreenToBlue; + + public byte RedToBlue; + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs index 97d5a57c9a..5bf313917a 100644 --- a/src/ImageSharp/Formats/WebP/WebPConstants.cs +++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs @@ -102,6 +102,11 @@ namespace SixLabors.ImageSharp.Formats.WebP /// public const int MaxNumberOfTransforms = 4; + /// + /// Maximum value of transformBits in VP8LEncoder. + /// + public const int MaxTransformBits = 6; + /// /// The bit to be written when next data to be read is a transform. /// diff --git a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs index 39f01e30d6..f53636cca7 100644 --- a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs @@ -35,6 +35,12 @@ namespace SixLabors.ImageSharp.Formats.WebP /// 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. /// @@ -119,6 +125,9 @@ namespace SixLabors.ImageSharp.Formats.WebP private void EncoderAnalyze(Image image, Vp8LEncoder enc) where TPixel : unmanaged, IPixel { + int method = 4; // TODO: method hardcoded to 4 for now. + int quality = 100; // TODO: quality is hardcoded for now. + bool useCache = true; // TODO: useCache is hardcoded for now. int width = image.Width; int height = image.Height; @@ -126,7 +135,6 @@ namespace SixLabors.ImageSharp.Formats.WebP var usePalette = this.AnalyzeAndCreatePalette(image, enc); // Empirical bit sizes. - int method = 4; // TODO: method hardcoded to 4 for now. enc.HistoBits = GetHistoBits(method, usePalette, width, height); enc.TransformBits = GetTransformBits(method, enc.HistoBits); @@ -151,13 +159,42 @@ namespace SixLabors.ImageSharp.Formats.WebP enc.UseSubtractGreenTransform = (entropyIdx == EntropyIx.SubGreen) || (entropyIdx == EntropyIx.SpatialSubGreen); enc.UsePredictorTransform = (entropyIdx == EntropyIx.Spatial) || (entropyIdx == EntropyIx.SpatialSubGreen); enc.UseCrossColorTransform = redAndBlueAlwaysZero ? false : enc.UsePredictorTransform; - enc.UseColorCache = false; + enc.CacheBits = 0; // 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. + //EncodeImageInternal(); } /// @@ -183,6 +220,51 @@ namespace SixLabors.ImageSharp.Formats.WebP 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 VP8 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 = true; // TODO: always true 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; @@ -239,7 +321,7 @@ namespace SixLabors.ImageSharp.Formats.WebP } var tokens = new HuffmanTreeToken[maxTokens]; - for(int i = 0; i < tokens.Length; i++) + for (int i = 0; i < tokens.Length; i++) { tokens[i] = new HuffmanTreeToken(); } @@ -721,6 +803,208 @@ namespace SixLabors.ImageSharp.Formats.WebP 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; + } + + enc.AllocateTransformBuffer(LosslessUtils.SubSampleSize(width, xBits), height); + + 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)); + } + 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; @@ -944,6 +1228,28 @@ namespace SixLabors.ImageSharp.Formats.WebP 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 @@ -951,6 +1257,12 @@ namespace SixLabors.ImageSharp.Formats.WebP 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) {