/// <summary> /// Initializes a new instance of the <see cref="WebpEncoderCore"/> class. /// </summary> /// <param name="options">The encoder options.</param> /// <param name="memoryAllocator">The memory manager.</param> public WebpEncoderCore(IWebpEncoderOptions options, MemoryAllocator memoryAllocator) { this.memoryAllocator = memoryAllocator; this.alphaCompression = options.UseAlphaCompression; this.fileFormat = options.FileFormat; this.quality = options.Quality; this.method = options.Method; this.entropyPasses = options.EntropyPasses; this.spatialNoiseShaping = options.SpatialNoiseShaping; this.filterStrength = options.FilterStrength; this.transparentColorMode = options.TransparentColorMode; this.nearLossless = options.NearLossless; this.nearLosslessQuality = options.NearLosslessQuality; }
/// <summary> /// 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. /// </summary> private static void GetResidual( int width, int height, Span <uint> upperRowSpan, Span <uint> currentRowSpan, Span <byte> maxDiffs, int mode, int xStart, int xEnd, int y, int maxQuantization, WebpTransparentColorMode transparentColorMode, bool usedSubtractGreen, bool nearLossless, Span <uint> output, Span <short> scratch) { if (transparentColorMode == WebpTransparentColorMode.Preserve) { PredictBatch(mode, xStart, y, xEnd - xStart, currentRowSpan, upperRowSpan, output, scratch); } else { #pragma warning disable SA1503 // Braces should not be omitted fixed(uint *currentRow = currentRowSpan) fixed(uint *upperRow = upperRowSpan) { 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(currentRow[x - 1], upperRow + x); break; case 3: predict = LosslessUtils.Predictor3(currentRow[x - 1], upperRow + x); break; case 4: predict = LosslessUtils.Predictor4(currentRow[x - 1], 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(currentRow[x - 1], upperRow + x); break; case 9: predict = LosslessUtils.Predictor9(currentRow[x - 1], upperRow + x); break; case 10: predict = LosslessUtils.Predictor10(currentRow[x - 1], upperRow + x); break; case 11: predict = LosslessUtils.Predictor11(currentRow[x - 1], upperRow + x, scratch); break; case 12: predict = LosslessUtils.Predictor12(currentRow[x - 1], upperRow + x); break; case 13: predict = LosslessUtils.Predictor13(currentRow[x - 1], upperRow + x); break; } } if (nearLossless) { 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 upperRow like below. } } else { residual = LosslessUtils.SubPixels(currentRow[x], predict); } 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 upperRow as well where top-right context is being read from. if (x == 0 && y != 0) { upperRow[width] = currentRow[0]; } } output[x - xStart] = residual; } } } }
/// <summary> /// 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. /// </summary> public static void ResidualImage( int width, int height, int bits, Span <uint> bgra, Span <uint> bgraScratch, Span <uint> image, int[][] histoArgb, int[][] bestHisto, bool nearLossless, int nearLosslessQuality, WebpTransparentColorMode transparentColorMode, bool usedSubtractGreen, bool lowEffort) { int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); int tilesPerCol = LosslessUtils.SubSampleSize(height, bits); int maxQuantization = 1 << LosslessUtils.NearLosslessBits(nearLosslessQuality); Span <short> scratch = stackalloc short[8]; // TODO: Can we optimize this? int[][] histo = new int[4][]; for (int i = 0; i < 4; i++) { histo[i] = new int[256]; } if (lowEffort) { for (int i = 0; i < tilesPerRow * tilesPerCol; i++) { image[i] = WebpConstants.ArgbBlack | (PredLowEffort << 8); } } else { for (int tileY = 0; tileY < tilesPerCol; tileY++) { for (int tileX = 0; tileX < tilesPerRow; tileX++) { int pred = GetBestPredictorForTile( width, height, tileX, tileY, bits, histo, bgraScratch, bgra, histoArgb, bestHisto, maxQuantization, transparentColorMode, usedSubtractGreen, nearLossless, image, scratch); image[(tileY * tilesPerRow) + tileX] = (uint)(WebpConstants.ArgbBlack | (pred << 8)); } } } CopyImageWithPrediction( width, height, bits, image, bgraScratch, bgra, maxQuantization, transparentColorMode, usedSubtractGreen, nearLossless, lowEffort); }
/// <summary> /// Returns best predictor and updates the accumulated histogram. /// If maxQuantization > 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). /// </summary> /// <returns>Best predictor.</returns> private static int GetBestPredictorForTile( int width, int height, int tileX, int tileY, int bits, int[][] accumulated, Span <uint> argbScratch, Span <uint> argb, int[][] histoArgb, int[][] bestHisto, int maxQuantization, WebpTransparentColorMode transparentColorMode, bool usedSubtractGreen, bool nearLossless, Span <uint> modes, Span <short> scratch) { 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 <uint> upperRow = argbScratch; Span <uint> currentRow = upperRow.Slice(width + 1); Span <byte> maxDiffs = MemoryMarshal.Cast <uint, byte>(currentRow.Slice(width + 1)); float bestDiff = MaxDiffCost; int bestMode = 0; uint[] residuals = new uint[1 << WebpConstants.MaxTransformBits]; for (int i = 0; i < 4; i++) { histoArgb[i].AsSpan().Clear(); bestHisto[i].AsSpan().Clear(); } for (int mode = 0; mode < numPredModes; mode++) { 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 <uint> src = argb.Slice(((startY - 1) * width) + contextStartX, maxX + haveLeft + 1); Span <uint> dst = currentRow.Slice(contextStartX); src.CopyTo(dst); } for (int relativeY = 0; relativeY < maxY; relativeY++) { int y = startY + relativeY; Span <uint> tmp = upperRow; upperRow = currentRow; currentRow = tmp; // Read currentRow. 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 currentRow). int offset = (y * width) + contextStartX; Span <uint> src = argb.Slice(offset, maxX + haveLeft + (y + 1 < height ? 1 : 0)); Span <uint> dst = currentRow.Slice(contextStartX); src.CopyTo(dst); if (nearLossless) { if (maxQuantization > 1 && y >= 1 && y + 1 < height) { MaxDiffsForRow(contextWidth, width, argb, offset, maxDiffs.Slice(contextStartX), usedSubtractGreen); } } GetResidual(width, height, upperRow, currentRow, maxDiffs, mode, startX, startX + maxX, y, maxQuantization, transparentColorMode, usedSubtractGreen, nearLossless, residuals, scratch); for (int relativeX = 0; relativeX < maxX; ++relativeX) { UpdateHisto(histoArgb, residuals[relativeX]); } } float curDiff = PredictionCostSpatialHistogram(accumulated, histoArgb); // Favor keeping the areas locally similar. if (mode == leftMode) { curDiff -= SpatialPredictorBias; } if (mode == aboveMode) { curDiff -= SpatialPredictorBias; } if (curDiff < bestDiff) { int[][] tmp = histoArgb; histoArgb = bestHisto; bestHisto = tmp; bestDiff = curDiff; bestMode = mode; } for (int i = 0; i < 4; i++) { histoArgb[i].AsSpan().Clear(); } } for (int i = 0; i < 4; i++) { for (int j = 0; j < 256; j++) { accumulated[i][j] += bestHisto[i][j]; } } return(bestMode); }