From 9dbd320b62f6c3409f0aefe7c9fb2a305550c838 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 6 Oct 2020 19:15:47 +0200 Subject: [PATCH] Use encoding method parameter --- .../Formats/WebP/IWebPEncoderOptions.cs | 2 + .../Formats/WebP/Lossless/Vp8LEncoder.cs | 44 +++++++++++-------- .../Formats/WebP/MetadataExtensions.cs | 3 ++ src/ImageSharp/Formats/WebP/WebPEncoder.cs | 4 +- .../Formats/WebP/WebPEncoderCore.cs | 8 +++- .../Formats/WebP/WebPEncoderTests.cs | 35 +++++++++++++-- 6 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs b/src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs index ab3757131c..f08ef8a7ff 100644 --- a/src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs +++ b/src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs @@ -19,11 +19,13 @@ namespace SixLabors.ImageSharp.Formats.WebP /// For lossy, 0 gives the smallest size and 100 the largest. For lossless, /// this parameter is the amount of effort put into the compression: 0 is the fastest but gives larger /// files compared to the slowest, but best, 100. + /// Defaults to 75. /// int Quality { get; } /// /// Gets the encoding method to use. Its a quality/speed trade-off (0=fast, 6=slower-better). + /// Defaults to 4. /// int Method { get; } diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs index 181fbaac15..8ead78f864 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs @@ -45,6 +45,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless /// private readonly int quality; + /// + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// + private readonly int method; + private const int ApplyPaletteGreedyMax = 4; private const int PaletteInvSizeBits = 11; @@ -58,12 +63,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless /// The width of the input image. /// The height of the input image. /// The encoding quality. - public Vp8LEncoder(MemoryAllocator memoryAllocator, int width, int height, int quality) + /// Quality/speed trade-off (0=fast, 6=slower-better). + public Vp8LEncoder(MemoryAllocator memoryAllocator, int width, int height, int quality, int method) { var pixelCount = width * height; int initialSize = pixelCount * 2; - this.quality = quality.Clamp(1, 100); + this.quality = quality.Clamp(0, 100); + this.method = method.Clamp(0, 6); this.bitWriter = new Vp8LBitWriter(initialSize); this.Bgra = memoryAllocator.Allocate(pixelCount); this.Palette = memoryAllocator.Allocate(WebPConstants.MaxPaletteSize); @@ -302,12 +309,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless if (this.UsePredictorTransform) { - this.ApplyPredictFilter(this.CurrentWidth, height, this.quality, this.UseSubtractGreenTransform); + this.ApplyPredictFilter(this.CurrentWidth, height, this.UseSubtractGreenTransform); } if (this.UseCrossColorTransform) { - this.ApplyCrossColorFilter(this.CurrentWidth, height, this.quality); + this.ApplyCrossColorFilter(this.CurrentWidth, height); } this.bitWriter.PutBits(0, 1); // No more transforms. @@ -333,7 +340,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless private CrunchConfig[] EncoderAnalyze(Image image, out bool redAndBlueAlwaysZero) where TPixel : unmanaged, IPixel { - int method = 4; // TODO: method hardcoded to 4 for now. int width = image.Width; int height = image.Height; @@ -341,8 +347,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless var usePalette = this.AnalyzeAndCreatePalette(image); // Empirical bit sizes. - this.HistoBits = GetHistoBits(method, usePalette, width, height); - this.TransformBits = GetTransformBits(method, this.HistoBits); + this.HistoBits = GetHistoBits(this.method, usePalette, width, height); + this.TransformBits = GetTransformBits(this.method, this.HistoBits); // Try out multiple LZ77 on images with few colors. var nlz77s = (this.PaletteSize > 0 && this.PaletteSize <= 16) ? 2 : 1; @@ -351,7 +357,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless bool doNotCache = false; var crunchConfigs = new List(); - if (method == 6 && this.quality == 100) + if (this.method == 6 && this.quality == 100) { doNotCache = true; @@ -370,7 +376,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless { // Only choose the guessed best transform. crunchConfigs.Add(new CrunchConfig { EntropyIdx = entropyIdx }); - if (this.quality >= 75 && method == 5) + if (this.quality >= 75 && this.method == 5) { // Test with and without color cache. doNotCache = true; @@ -423,7 +429,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } // Calculate backward references from BGRA image. - BackwardReferenceEncoder.HashChainFill(hashChain, bgra, quality, width, height); + BackwardReferenceEncoder.HashChainFill(hashChain, bgra, this.quality, width, height); Vp8LBitWriter bitWriterBest = config.SubConfigs.Count > 1 ? this.bitWriter.Clone() : this.bitWriter; @@ -433,7 +439,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless width, height, bgra, - quality, + this.quality, subConfig.Lz77, ref cacheBits, hashChain, @@ -455,7 +461,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } // Build histogram image and symbols from backward references. - HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, quality, histogramBits, cacheBits, histogramImage, tmpHisto, histogramSymbols); + HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, this.quality, histogramBits, cacheBits, histogramImage, tmpHisto, histogramSymbols); // Create Huffman bit lengths and codes for each histogram image. var histogramImageSize = histogramImage.Count; @@ -498,7 +504,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } this.bitWriter.PutBits((uint)(histogramBits - 2), 3); - this.EncodeImageNoHuffman(histogramBgra, hashChain, refsTmp, refsArray[2], LosslessUtils.SubSampleSize(width, histogramBits), LosslessUtils.SubSampleSize(height, histogramBits), quality); + this.EncodeImageNoHuffman(histogramBgra, hashChain, refsTmp, refsArray[2], LosslessUtils.SubSampleSize(width, histogramBits), LosslessUtils.SubSampleSize(height, histogramBits), this.quality); } // Store Huffman codes. @@ -572,7 +578,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless LosslessUtils.SubtractGreenFromBlueAndRed(this.Bgra.GetSpan(), width * height); } - private void ApplyPredictFilter(int width, int height, int quality, bool usedSubtractGreen) + private void ApplyPredictFilter(int width, int height, bool usedSubtractGreen) { int nearLosslessStrength = 100; // TODO: for now always 100 bool exact = false; // TODO: always false for now. @@ -586,22 +592,22 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless this.bitWriter.PutBits((uint)Vp8LTransformType.PredictorTransform, 2); this.bitWriter.PutBits((uint)(predBits - 2), 3); - this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, quality); + this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality); } - private void ApplyCrossColorFilter(int width, int height, int quality) + private void ApplyCrossColorFilter(int width, int height) { int colorTransformBits = this.TransformBits; int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits); int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits); - PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, quality, this.Bgra.GetSpan(), this.TransformData.GetSpan()); + PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.Bgra.GetSpan(), this.TransformData.GetSpan()); this.bitWriter.PutBits(WebPConstants.TransformPresent, 1); this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2); this.bitWriter.PutBits((uint)(colorTransformBits - 2), 3); - this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, quality); + this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality); } private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs refsTmp1, Vp8LBackwardRefs refsTmp2, int width, int height, int quality) @@ -1488,7 +1494,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless var huffTree = new HuffmanTree[3 * maxNumSymbols]; for (int i = 0; i < huffTree.Length; i++) { - huffTree[i] = new HuffmanTree(); + huffTree[i] = default; } for (int i = 0; i < histogramImage.Count; i++) diff --git a/src/ImageSharp/Formats/WebP/MetadataExtensions.cs b/src/ImageSharp/Formats/WebP/MetadataExtensions.cs index 3d0aca6db6..a0a4674d10 100644 --- a/src/ImageSharp/Formats/WebP/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/WebP/MetadataExtensions.cs @@ -1,3 +1,6 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + using SixLabors.ImageSharp.Formats.WebP; using SixLabors.ImageSharp.Metadata; diff --git a/src/ImageSharp/Formats/WebP/WebPEncoder.cs b/src/ImageSharp/Formats/WebP/WebPEncoder.cs index c6770daebc..0ee881880c 100644 --- a/src/ImageSharp/Formats/WebP/WebPEncoder.cs +++ b/src/ImageSharp/Formats/WebP/WebPEncoder.cs @@ -18,10 +18,10 @@ namespace SixLabors.ImageSharp.Formats.WebP public bool Lossy { get; set; } /// - public int Quality { get; set; } + public int Quality { get; set; } = 75; /// - public int Method { get; set; } + public int Method { get; set; } = 4; /// public bool AlphaCompression { get; set; } diff --git a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs index a5bce7f8c1..8943ce9bd4 100644 --- a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs @@ -43,6 +43,11 @@ namespace SixLabors.ImageSharp.Formats.WebP /// private readonly int quality; + /// + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// + private readonly int method; + /// /// Initializes a new instance of the class. /// @@ -54,6 +59,7 @@ namespace SixLabors.ImageSharp.Formats.WebP this.alphaCompression = options.AlphaCompression; this.lossy = options.Lossy; this.quality = options.Quality; + this.method = options.Method; } /// @@ -78,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.WebP } else { - var enc = new Vp8LEncoder(this.memoryAllocator, image.Width, image.Height, this.quality); + var enc = new Vp8LEncoder(this.memoryAllocator, image.Width, image.Height, this.quality, this.method); enc.Encode(image, stream); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebPEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebPEncoderTests.cs index 44ffa2be3d..4777adb4ff 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebPEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebPEncoderTests.cs @@ -1,3 +1,6 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + using SixLabors.ImageSharp.Formats.WebP; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -12,17 +15,43 @@ namespace SixLabors.ImageSharp.Tests.Formats.WebP [WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32, 100)] [WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32, 80)] [WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32, 20)] - public void Encode_Lossless_Works(TestImageProvider provider, int quality) + public void Encode_Lossless_WithDifferentQuality_Works(TestImageProvider provider, int quality) + where TPixel : unmanaged, IPixel + { + var encoder = new WebPEncoder() + { + Lossy = false, + Quality = quality + }; + + using (Image image = provider.GetImage()) + { + var testOutputDetails = string.Concat("lossless", "_q", quality); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); + } + } + + [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)] + public void Encode_Lossless_WithDifferentMethods_Works(TestImageProvider provider, int method) where TPixel : unmanaged, IPixel { var encoder = new WebPEncoder() { - Lossy = false + Lossy = false, + Method = method, + Quality = 100 }; using (Image image = provider.GetImage()) { - var testOutputDetails = string.Concat("lossless", "_", quality); + var testOutputDetails = string.Concat("lossless", "_m", method); image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); } }