From be36ff691155c5b6119f8d8255b85decd19493c2 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 9 Jul 2021 20:58:02 +0200 Subject: [PATCH] Add near lossless encoding mode --- .../Formats/WebP/IWebpEncoderOptions.cs | 12 ++ .../Formats/WebP/Lossless/LosslessUtils.cs | 1 + .../Formats/WebP/Lossless/NearLosslessEnc.cs | 129 ++++++++++++++++++ .../Formats/WebP/Lossless/PredictorEncoder.cs | 89 ++++++++---- .../Formats/WebP/Lossless/Vp8LEncoder.cs | 44 ++++-- src/ImageSharp/Formats/WebP/WebpEncoder.cs | 6 + .../Formats/WebP/WebpEncoderCore.cs | 33 ++++- .../Formats/GeneralFormatTests.cs | 3 +- .../Formats/WebP/WebpEncoderTests.cs | 117 +++++++++------- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/WebP/rgb_pattern.png | 3 + 11 files changed, 349 insertions(+), 89 deletions(-) create mode 100644 src/ImageSharp/Formats/WebP/Lossless/NearLosslessEnc.cs create mode 100644 tests/Images/Input/WebP/rgb_pattern.png diff --git a/src/ImageSharp/Formats/WebP/IWebpEncoderOptions.cs b/src/ImageSharp/Formats/WebP/IWebpEncoderOptions.cs index dad67f804..6c8449772 100644 --- a/src/ImageSharp/Formats/WebP/IWebpEncoderOptions.cs +++ b/src/ImageSharp/Formats/WebP/IWebpEncoderOptions.cs @@ -54,5 +54,17 @@ namespace SixLabors.ImageSharp.Formats.Webp /// The default value is false. /// bool Exact { get; } + + /// + /// Gets a value indicating whether near lossless mode should be used. + /// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality. + /// + bool NearLossless { get; } + + /// + /// Gets the quality of near-lossless image preprocessing. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// The typical value is around 60. Note that lossy with -q 100 can at times yield better results. + /// + int NearLosslessQuality { get; } } } diff --git a/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs index d6ba6e481..4b3cce9af 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs @@ -770,6 +770,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless m.RedToBlue = (byte)((colorCode >> 16) & 0xff); } + // Converts near lossless quality into max number of bits shaved off. // 100 -> 0 // 80..99 -> 1 // 60..79 -> 2 diff --git a/src/ImageSharp/Formats/WebP/Lossless/NearLosslessEnc.cs b/src/ImageSharp/Formats/WebP/Lossless/NearLosslessEnc.cs new file mode 100644 index 000000000..4c035a647 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Lossless/NearLosslessEnc.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Webp.Lossless; + +namespace SixLabors.ImageSharp.Formats.WebP.Lossless +{ + /// + /// Near-lossless image preprocessing adjusts pixel values to help compressibility with a guarantee + /// of maximum deviation between original and resulting pixel values. + /// + internal static class NearLosslessEnc + { + private const int MinDimForNearLossless = 64; + + public static void ApplyNearLossless(int xSize, int ySize, int quality, Span argbSrc, Span argbDst, int stride) + { + uint[] copyBuffer = new uint[xSize * 3]; + int limitBits = LosslessUtils.NearLosslessBits(quality); + + // For small icon images, don't attempt to apply near-lossless compression. + if ((xSize < MinDimForNearLossless && ySize < MinDimForNearLossless) || ySize < 3) + { + for (int i = 0; i < ySize; ++i) + { + argbSrc.Slice(i * stride, xSize).CopyTo(argbDst.Slice(i * xSize, xSize)); + } + + return; + } + + NearLossless(xSize, ySize, argbSrc, stride, limitBits, copyBuffer, argbDst); + for (int i = limitBits - 1; i != 0; --i) + { + NearLossless(xSize, ySize, argbDst, xSize, i, copyBuffer, argbDst); + } + } + + // Adjusts pixel values of image with given maximum error. + private static void NearLossless(int xSize, int ySize, Span argbSrc, int stride, int limitBits, Span copyBuffer, Span argbDst) + { + int x, y; + int limit = 1 << limitBits; + Span prevRow = copyBuffer; + Span currRow = copyBuffer.Slice(xSize, xSize); + Span nextRow = copyBuffer.Slice(xSize * 2, xSize); + argbSrc.Slice(0, xSize).CopyTo(currRow); + argbSrc.Slice(xSize, xSize).CopyTo(nextRow); + + int srcOffset = 0; + int dstOffset = 0; + for (y = 0; y < ySize; ++y) + { + if (y == 0 || y == ySize - 1) + { + argbSrc.Slice(srcOffset, xSize).CopyTo(argbDst.Slice(dstOffset, xSize)); + } + else + { + argbSrc.Slice(srcOffset + stride, xSize).CopyTo(nextRow); + argbDst[dstOffset] = argbSrc[srcOffset]; + argbDst[dstOffset + xSize - 1] = argbSrc[srcOffset + xSize - 1]; + for (x = 1; x < xSize - 1; ++x) + { + if (IsSmooth(prevRow, currRow, nextRow, x, limit)) + { + argbDst[dstOffset + x] = currRow[x]; + } + else + { + argbDst[dstOffset + x] = ClosestDiscretizedArgb(currRow[x], limitBits); + } + } + } + + Span temp = prevRow; + prevRow = currRow; + currRow = nextRow; + nextRow = temp; + srcOffset += stride; + dstOffset += xSize; + } + } + + // Applies FindClosestDiscretized to all channels of pixel. + private static uint ClosestDiscretizedArgb(uint a, int bits) => + (FindClosestDiscretized(a >> 24, bits) << 24) | + (FindClosestDiscretized((a >> 16) & 0xff, bits) << 16) | + (FindClosestDiscretized((a >> 8) & 0xff, bits) << 8) | + FindClosestDiscretized(a & 0xff, bits); + + private static uint FindClosestDiscretized(uint a, int bits) + { + uint mask = (1u << bits) - 1; + uint biased = a + (mask >> 1) + ((a >> bits) & 1); + if (biased > 0xff) + { + return 0xff; + } + + return biased & ~mask; + } + + private static bool IsSmooth(Span prevRow, Span currRow, Span nextRow, int ix, int limit) + { + // Check that all pixels in 4-connected neighborhood are smooth. + return IsNear(currRow[ix], currRow[ix - 1], limit) && + IsNear(currRow[ix], currRow[ix + 1], limit) && + IsNear(currRow[ix], prevRow[ix], limit) && + IsNear(currRow[ix], nextRow[ix], limit); + } + + // Checks if distance between corresponding channel values of pixels a and b is within the given limit. + private static bool IsNear(uint a, uint b, int limit) + { + for (int k = 0; k < 4; ++k) + { + int delta = (int)((a >> (k * 8)) & 0xff) - (int)((b >> (k * 8)) & 0xff); + if (delta >= limit || delta <= -limit) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs index 9f666ff6a..e060bbc10 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs @@ -39,6 +39,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless Span bgra, Span bgraScratch, Span image, + bool nearLossless, int nearLosslessQuality, bool exact, bool usedSubtractGreen) @@ -71,6 +72,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless maxQuantization, exact, usedSubtractGreen, + nearLossless, image); image[(tileY * tilesPerRow) + tileX] = (uint)(WebpConstants.ArgbBlack | (pred << 8)); @@ -86,7 +88,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless bgra, maxQuantization, exact, - usedSubtractGreen); + usedSubtractGreen, + nearLossless); } public static void ColorSpaceTransform(int width, int height, int bits, int quality, Span bgra, Span image) @@ -175,6 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless int maxQuantization, bool exact, bool usedSubtractGreen, + bool nearLossless, Span modes) { const int numPredModes = 14; @@ -242,18 +246,20 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless // 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). - Span src = argb.Slice((y * width) + contextStartX, maxX + haveLeft + ((y + 1) < height ? 1 : 0)); + int offset = (y * width) + contextStartX; + Span src = argb.Slice(offset, maxX + haveLeft + ((y + 1) < height ? 1 : 0)); Span dst = currentRow.Slice(contextStartX); src.CopyTo(dst); - // TODO: Source wraps this in conditional - // WEBP_NEAR_LOSSLESS == 1 - if (maxQuantization > 1 && y >= 1 && y + 1 < height) + if (nearLossless) { - MaxDiffsForRow(contextWidth, width, argb.Slice((y * width) + contextStartX), maxDiffs.Slice(contextStartX), usedSubtractGreen); + 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, exact, usedSubtractGreen, residuals); + GetResidual(width, height, upperRow, currentRow, maxDiffs, mode, startX, startX + maxX, y, maxQuantization, exact, usedSubtractGreen, nearLossless, residuals); for (int relativeX = 0; relativeX < maxX; ++relativeX) { UpdateHisto(histoArgb, residuals[relativeX]); @@ -316,6 +322,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless int maxQuantization, bool exact, bool usedSubtractGreen, + bool nearLossless, Span output) { if (exact) @@ -388,18 +395,25 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless } } - if (maxQuantization == 1 || mode == 0 || y == 0 || y == height - 1 || x == 0 || x == width - 1) + if (nearLossless) { - residual = LosslessUtils.SubPixels(currentRow[x], predict); + 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 = 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. + residual = LosslessUtils.SubPixels(currentRow[x], predict); } if ((currentRow[x] & MaskAlpha) == 0) @@ -534,7 +548,17 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless /// 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) + private static void CopyImageWithPrediction( + int width, + int height, + int bits, + Span modes, + Span argbScratch, + Span argb, + int maxQuantization, + bool exact, + bool usedSubtractGreen, + bool nearLossless) { int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); @@ -566,7 +590,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless lowerMaxDiffs = tmp8; if (y + 2 < height) { - MaxDiffsForRow(width, width, argb.Slice((y + 1) * width), lowerMaxDiffs, usedSubtractGreen); + MaxDiffsForRow(width, width, argb, (y + 1) * width, lowerMaxDiffs, usedSubtractGreen); } } @@ -592,6 +616,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless maxQuantization, exact, usedSubtractGreen, + nearLossless, argb.Slice((y * width) + x)); x = xEnd; @@ -687,15 +712,15 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless } } - private static void MaxDiffsForRow(int width, int stride, Span argb, Span maxDiffs, bool usedSubtractGreen) + private static void MaxDiffsForRow(int width, int stride, Span argb, int offset, Span maxDiffs, bool usedSubtractGreen) { if (width <= 2) { return; } - uint current = argb[0]; - uint right = argb[1]; + uint current = argb[offset]; + uint right = argb[offset + 1]; if (usedSubtractGreen) { current = AddGreenToBlueAndRed(current); @@ -704,11 +729,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless for (int x = 1; x < width - 1; ++x) { - uint up = argb[-stride + x]; - uint down = argb[stride + x]; + uint up = argb[offset - stride + x]; + uint down = argb[offset + stride + x]; uint left = current; current = right; - right = argb[x + 1]; + right = argb[offset + x + 1]; if (usedSubtractGreen) { up = AddGreenToBlueAndRed(up); @@ -874,12 +899,14 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless if ((byte)greenToRed == prevX.GreenToRed) { - curDiff -= 3; // Favor keeping the areas locally similar. + // Favor keeping the areas locally similar. + curDiff -= 3; } if ((byte)greenToRed == prevY.GreenToRed) { - curDiff -= 3; // Favor keeping the areas locally similar. + // Favor keeping the areas locally similar. + curDiff -= 3; } if (greenToRed == 0) @@ -898,22 +925,26 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless double curDiff = PredictionCostCrossColor(accumulatedBlueHisto, histo); if ((byte)greenToBlue == prevX.GreenToBlue) { - curDiff -= 3; // Favor keeping the areas locally similar. + // Favor keeping the areas locally similar. + curDiff -= 3; } if ((byte)greenToBlue == prevY.GreenToBlue) { - curDiff -= 3; // Favor keeping the areas locally similar. + // Favor keeping the areas locally similar. + curDiff -= 3; } if ((byte)redToBlue == prevX.RedToBlue) { - curDiff -= 3; // Favor keeping the areas locally similar. + // Favor keeping the areas locally similar. + curDiff -= 3; } if ((byte)redToBlue == prevY.RedToBlue) { - curDiff -= 3; // Favor keeping the areas locally similar. + // Favor keeping the areas locally similar. + curDiff -= 3; } if (greenToBlue == 0) diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs index 5b86cd4c2..678eb696a 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.WebP.Lossless; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -60,6 +61,16 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless /// private readonly bool exact; + /// + /// Indicating whether near lossless mode should be used. + /// + private readonly bool nearLossless; + + /// + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// + private readonly int nearLosslessQuality; + private const int ApplyPaletteGreedyMax = 4; private const int PaletteInvSizeBits = 11; @@ -76,7 +87,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless /// The encoding quality. /// Quality/speed trade-off (0=fast, 6=slower-better). /// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible RGB information for better compression. - public Vp8LEncoder(MemoryAllocator memoryAllocator, Configuration configuration, int width, int height, int quality, int method, bool exact) + /// Indicating whether near lossless mode should be used. + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + public Vp8LEncoder(MemoryAllocator memoryAllocator, Configuration configuration, int width, int height, int quality, int method, bool exact, bool nearLossless, int nearLosslessQuality) { int pixelCount = width * height; int initialSize = pixelCount * 2; @@ -86,6 +99,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.quality = Numerics.Clamp(quality, 0, 100); this.method = Numerics.Clamp(method, 0, 6); this.exact = exact; + this.nearLossless = nearLossless; + this.nearLosslessQuality = Numerics.Clamp(nearLosslessQuality, 0, 100); this.bitWriter = new Vp8LBitWriter(initialSize); this.Bgra = memoryAllocator.Allocate(pixelCount); this.EncodedData = memoryAllocator.Allocate(pixelCount); @@ -251,7 +266,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless int width = image.Width; int height = image.Height; - ReadOnlySpan bgra = this.Bgra.GetSpan(); + Span bgra = this.Bgra.GetSpan(); Span encodedData = this.EncodedData.GetSpan(); // Analyze image (entropy, numPalettes etc). @@ -278,7 +293,16 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.CacheBits = 0; this.ClearRefs(); - // TODO: Apply near-lossless preprocessing. + if (this.nearLossless) + { + // Apply near-lossless preprocessing. + bool useNearLossless = (this.nearLosslessQuality < 100) && !this.UsePalette && !this.UsePredictorTransform; + if (useNearLossless) + { + this.AllocateTransformBuffer(width, height); + NearLosslessEnc.ApplyNearLossless(width, height, this.nearLosslessQuality, bgra, bgra, width); + } + } // Encode palette. if (this.UsePalette) @@ -301,7 +325,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless if (this.UsePredictorTransform) { - this.ApplyPredictFilter(this.CurrentWidth, height, this.UseSubtractGreenTransform); + this.ApplyPredictFilter(this.CurrentWidth, height); } if (this.UseCrossColorTransform) @@ -618,9 +642,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless LosslessUtils.SubtractGreenFromBlueAndRed(this.EncodedData.GetSpan()); } - private void ApplyPredictFilter(int width, int height, bool usedSubtractGreen) + private void ApplyPredictFilter(int width, int height) { - int nearLosslessStrength = 100; // TODO: for now always 100 + // We disable near-lossless quantization if palette is used. + int nearLosslessStrength = this.UsePalette ? 100 : this.nearLosslessQuality; int predBits = this.TransformBits; int transformWidth = LosslessUtils.SubSampleSize(width, predBits); int transformHeight = LosslessUtils.SubSampleSize(height, predBits); @@ -632,9 +657,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.EncodedData.GetSpan(), this.BgraScratch.GetSpan(), this.TransformData.GetSpan(), + this.nearLossless, nearLosslessStrength, this.exact, - usedSubtractGreen); + this.UseSubtractGreenTransform); this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); this.bitWriter.PutBits((uint)Vp8LTransformType.PredictorTransform, 2); @@ -1709,10 +1735,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless { // 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 bgraScratchSize = 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.BgraScratch = this.memoryAllocator.Allocate(bgraScratchSize); this.TransformData = this.memoryAllocator.Allocate(transformDataSize); this.CurrentWidth = width; } diff --git a/src/ImageSharp/Formats/WebP/WebpEncoder.cs b/src/ImageSharp/Formats/WebP/WebpEncoder.cs index ae0fb5122..eb7148386 100644 --- a/src/ImageSharp/Formats/WebP/WebpEncoder.cs +++ b/src/ImageSharp/Formats/WebP/WebpEncoder.cs @@ -35,6 +35,12 @@ namespace SixLabors.ImageSharp.Formats.Webp /// public bool Exact { get; set; } + /// + public bool NearLossless { get; set; } + + /// + public int NearLosslessQuality { get; set; } = 100; + /// public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/WebP/WebpEncoderCore.cs b/src/ImageSharp/Formats/WebP/WebpEncoderCore.cs index 09a319de8..b515bd48b 100644 --- a/src/ImageSharp/Formats/WebP/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebpEncoderCore.cs @@ -58,6 +58,16 @@ namespace SixLabors.ImageSharp.Formats.Webp /// private readonly bool exact; + /// + /// Indicating whether near lossless mode should be used. + /// + private readonly bool nearLossless; + + /// + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// + private readonly int nearLosslessQuality; + /// /// The global configuration. /// @@ -78,6 +88,8 @@ namespace SixLabors.ImageSharp.Formats.Webp this.entropyPasses = options.EntropyPasses; this.filterStrength = options.FilterStrength; this.exact = options.Exact; + this.nearLossless = options.NearLossless; + this.nearLosslessQuality = options.NearLosslessQuality; } /// @@ -97,12 +109,29 @@ namespace SixLabors.ImageSharp.Formats.Webp if (this.lossy) { - using var enc = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, image.Height, this.quality, this.method, this.entropyPasses, this.filterStrength); + using var enc = new Vp8Encoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.method, + this.entropyPasses, + this.filterStrength); enc.Encode(image, stream); } else { - using var enc = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, image.Height, this.quality, this.method, this.exact); + using var enc = new Vp8LEncoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.method, + this.exact, + this.nearLossless, + this.nearLosslessQuality); enc.Encode(image, stream); } } diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index c0843a51b..bf13a9097 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -16,6 +16,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats { + [Collection("RunSerial")] public class GeneralFormatTests { /// @@ -152,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests.Formats using (FileStream output = File.OpenWrite(Path.Combine(path, $"{file.FileNameWithoutExtension}.tiff"))) { - image.SaveAsTga(output); + image.SaveAsTiff(output); } } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 4d9ff2cfb..6173ebfc3 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -80,6 +80,75 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); } + [Theory] + [WithFile(RgbTestPattern, PixelTypes.Rgba32, 85)] + [WithFile(RgbTestPattern, PixelTypes.Rgba32, 60)] + [WithFile(RgbTestPattern, PixelTypes.Rgba32, 40)] + [WithFile(RgbTestPattern, PixelTypes.Rgba32, 20)] + [WithFile(RgbTestPattern, PixelTypes.Rgba32, 10)] + public void Encode_Lossless_WithNearLosslessFlag_Works(TestImageProvider provider, int nearLosslessQuality) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + Lossy = false, + NearLossless = true, + NearLosslessQuality = nearLosslessQuality + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("nearlossless", "_q", nearLosslessQuality); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(nearLosslessQuality)); + } + + [Theory] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 0)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 1)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 2)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 3)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 4)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 5)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 6)] + [WithFile(Lossy.Alpha1, PixelTypes.Rgba32, 4)] + public void Encode_Lossless_WithExactFlag_Works(TestImageProvider provider, int method) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + Lossy = false, + Method = method, + Exact = true + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossless", "_m", method); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); + } + + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + [WithFile(TestPatternOpaqueSmall, PixelTypes.Rgba32)] + public void Encode_Lossless_WorksWithTestPattern(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + var encoder = new WebpEncoder() { Lossy = false }; + image.VerifyEncoder(provider, "webp", string.Empty, encoder); + } + + [Fact] + public void Encode_Lossless_OneByOnePixel_Works() + { + // Just make sure, encoding 1 pixel by 1 pixel does not throw an exception. + using var image = new Image(1, 1); + var encoder = new WebpEncoder() { Lossy = false }; + using (var memStream = new MemoryStream()) + { + image.SaveAsWebp(memStream, encoder); + } + } + [Theory] [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 100)] [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 75)] @@ -148,30 +217,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(quality)); } - [Theory] - [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 0)] - [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 1)] - [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 2)] - [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 3)] - [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 4)] - [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 5)] - [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 6)] - [WithFile(Lossy.Alpha1, PixelTypes.Rgba32, 4)] - public void Encode_Lossless_WithExactFlag_Works(TestImageProvider provider, int method) - where TPixel : unmanaged, IPixel - { - var encoder = new WebpEncoder() - { - Lossy = false, - Method = method, - Exact = true - }; - - using Image image = provider.GetImage(); - string testOutputDetails = string.Concat("lossless", "_m", method); - image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); - } - [Theory] [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] [WithFile(TestPatternOpaqueSmall, PixelTypes.Rgba32)] @@ -184,30 +229,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.Tolerant(0.04f)); } - [Theory] - [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] - [WithFile(TestPatternOpaqueSmall, PixelTypes.Rgba32)] - public void Encode_Lossless_WorksWithTestPattern(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(); - - var encoder = new WebpEncoder() { Lossy = false }; - image.VerifyEncoder(provider, "webp", string.Empty, encoder); - } - - [Fact] - public void Encode_Lossless_OneByOnePixel_Works() - { - // Just make sure, encoding 1 pixel by 1 pixel does not throw an exception. - using var image = new Image(1, 1); - var encoder = new WebpEncoder() { Lossy = false }; - using (var memStream = new MemoryStream()) - { - image.SaveAsWebp(memStream, encoder); - } - } - private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 5c86ffcd3..524ea9849 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -508,6 +508,7 @@ namespace SixLabors.ImageSharp.Tests // Test pattern images for testing the encoder. public const string TestPatternOpaque = "WebP/testpattern_opaque.png"; public const string TestPatternOpaqueSmall = "WebP/testpattern_opaque_small.png"; + public const string RgbTestPattern = "WebP/rgb_pattern.png"; // Test image for encoding image with a palette. public const string Flag = "WebP/flag_of_germany.png"; diff --git a/tests/Images/Input/WebP/rgb_pattern.png b/tests/Images/Input/WebP/rgb_pattern.png new file mode 100644 index 000000000..d3c59cf88 --- /dev/null +++ b/tests/Images/Input/WebP/rgb_pattern.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1ffed99a3cc701fff7f63cdca32c437a3e03d2a8a178380744190636decb0f8 +size 12453