From c72dd42406d8efdcd956b79bba252c7351db0274 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 17 May 2020 18:08:56 +0200 Subject: [PATCH] Analyze and create palette --- .../Formats/WebP/BitWriter/Vp8BitWriter.cs | 34 +++ .../Formats/WebP/BitWriter/Vp8LBitWriter.cs | 88 +++++++ .../Formats/WebP/IWebPEncoderOptions.cs | 23 ++ .../Formats/WebP/Lossless/LosslessUtils.cs | 22 +- .../Formats/WebP/Lossless/Vp8LEncoder.cs | 36 +++ src/ImageSharp/Formats/WebP/WebPConstants.cs | 15 ++ .../Formats/WebP/WebPDecoderCore.cs | 2 +- src/ImageSharp/Formats/WebP/WebPEncoder.cs | 12 + .../Formats/WebP/WebPEncoderCore.cs | 221 ++++++++++++++++++ 9 files changed, 441 insertions(+), 12 deletions(-) create mode 100644 src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs index 974d18c9a..2764abc30 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs @@ -8,5 +8,39 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter /// internal class Vp8BitWriter { + private uint range; + + private uint value; + + /// + /// Number of outstanding bits. + /// + private int run; + + /// + /// Number of pending bits. + /// + private int nbBits; + + private byte[] buffer; + + private int pos; + + private int maxPos; + + private bool error; + + public Vp8BitWriter(int expectedSize) + { + this.range = 255 - 1; + this.value = 0; + this.run = 0; + this.nbBits = -8; + this.pos = 0; + this.maxPos = 0; + this.error = false; + + //BitWriterResize(expected_size); + } } } diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs index 925592fb6..fecb681d1 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the GNU Affero General Public License, Version 3. +using System; + namespace SixLabors.ImageSharp.Formats.WebP.BitWriter { /// @@ -8,5 +10,91 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter /// internal class Vp8LBitWriter { + /// + /// This is the minimum amount of size the memory buffer is guaranteed to grow when extra space is needed. + /// + private const int MinExtraSize = 32768; + + private const int WriterBytes = 4; + + private const int WriterBits = 32; + + private const int WriterMaxBits = 64; + + /// + /// Bit accumulator. + /// + private ulong bits; + + /// + /// Number of bits used in accumulator. + /// + private int used; + + /// + /// Buffer to write to. + /// + private byte[] buffer; + + /// + /// Current write position. + /// + private int cur; + + private int end; + + private bool error; + + public Vp8LBitWriter(int expectedSize) + { + this.buffer = new byte[expectedSize]; + } + + /// + /// This function writes bits into bytes in increasing addresses (little endian), + /// and within a byte least-significant-bit first. + /// This function can write up to 32 bits in one go, but VP8LBitReader can only + /// read 24 bits max (VP8L_MAX_NUM_BIT_READ). + /// + public void PutBits(uint bits, int nBits) + { + if (nBits > 0) + { + if (this.used >= 32) + { + this.PutBitsFlushBits(); + } + + this.bits |= bits << this.used; + this.used += nBits; + } + } + + /// + /// Internal function for PutBits flushing 32 bits from the written state. + /// + private void PutBitsFlushBits() + { + // If needed, make some room by flushing some bits out. + if (this.cur + WriterBytes > this.end) + { + var extraSize = (this.end - this.cur) + MinExtraSize; + if (!BitWriterResize(extraSize)) + { + this.error = true; + return; + } + } + + //*(vp8l_wtype_t*)bw->cur_ = (vp8l_wtype_t)WSWAP((vp8l_wtype_t)bw->bits_); + this.cur += WriterBytes; + this.bits >>= WriterBits; + this.used -= WriterBits; + } + + private bool BitWriterResize(int extraSize) + { + return true; + } } } diff --git a/src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs b/src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs index e29a446c9..f87dd954f 100644 --- a/src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs +++ b/src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs @@ -8,5 +8,28 @@ namespace SixLabors.ImageSharp.Formats.WebP /// internal interface IWebPEncoderOptions { + /// + /// Gets a value indicating whether lossless compression should be used. + /// If false, lossy compression will be used. + /// + bool Lossless { get; } + + /// + /// Gets the compression quality. Between 0 and 100. + /// 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. + /// + float Quality { get; } + + /// + /// Gets a value indicating whether the alpha plane should be compressed with WebP lossless format. + /// + bool AlphaCompression { get; } + + /// + /// Gets the number of entropy-analysis passes (in [1..10]). + /// + int EntropyPasses { get; } } } diff --git a/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs index 738ed17bb..d0cbd1e0a 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs @@ -294,6 +294,17 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless } } + /// + /// Difference of each component, mod 256. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static uint SubPixels(uint a, uint b) + { + uint alphaAndGreen = 0x00ff00ffu + (a & 0xff00ff00u) - (b & 0xff00ff00u); + uint redAndBlue = 0xff00ff00u + (a & 0x00ff00ffu) - (b & 0x00ff00ffu); + return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); + } + private static void PredictorAdd0(Span input, int startIdx, int numberOfPixels, Span output) { int endIdx = startIdx + numberOfPixels; @@ -629,17 +640,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); } - /// - /// Difference of each component, mod 256. - /// - [MethodImpl(InliningOptions.ShortMethod)] - private static uint SubPixels(uint a, uint b) - { - uint alphaAndGreen = 0x00ff00ffu + (a & 0xff00ff00u) - (b & 0xff00ff00u); - uint redAndBlue = 0xff00ff00u + (a & 0x00ff00ffu) - (b & 0x00ff00ffu); - return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); - } - [MethodImpl(InliningOptions.ShortMethod)] private static uint GetArgbIndex(uint idx) { diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs new file mode 100644 index 000000000..efa0acc09 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the GNU Affero General Public License, Version 3. + +namespace SixLabors.ImageSharp.Formats.WebP.Lossless +{ + /// + /// Encoder for lossless webp images. + /// + internal class Vp8LEncoder + { + /// + /// Gets a value indicating whether to use the cross color transform. + /// + public bool UseCrossColorTransform { get; } + + /// + /// Gets a value indicating whether to use the substract green transform. + /// + public bool UseSubtractGreenTransform { get; } + + /// + /// Gets a value indicating whether to use the predictor transform. + /// + public bool UsePredictorTransform { get; } + + /// + /// Gets a value indicating whether to use color indexing transform. + /// + public bool UsePalette { get; } + + /// + /// Gets the palette size. + /// + public int PaletteSize { get; } + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs index a2e742b68..c696d19b1 100644 --- a/src/ImageSharp/Formats/WebP/WebPConstants.cs +++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs @@ -67,6 +67,16 @@ namespace SixLabors.ImageSharp.Formats.WebP /// public const int Vp8LImageSizeBits = 14; + /// + /// The Vp8L version 0. + /// + public const int Vp8LVersion = 0; + + /// + /// The maximum number of colors for a paletted images. + /// + public const int MaxPaletteSize = 256; + /// /// Maximum number of color cache bits. /// @@ -77,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.WebP /// public const int MaxNumberOfTransforms = 4; + /// + /// The maximum allowed width or height of a webp image. + /// + public const int MaxDimension = 16383; + public const int MaxAllowedCodeLength = 15; public const int DefaultCodeLength = 8; diff --git a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs index 6953dffce..8b0a32fab 100644 --- a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs @@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Formats.WebP this.Metadata = new ImageMetadata(); this.currentStream = stream; - uint fileSize = this.ReadImageHeader(); + this.ReadImageHeader(); using WebPImageInfo imageInfo = this.ReadVp8Info(); if (imageInfo.Features != null && imageInfo.Features.Animation) { diff --git a/src/ImageSharp/Formats/WebP/WebPEncoder.cs b/src/ImageSharp/Formats/WebP/WebPEncoder.cs index b201e0e8d..062756d0d 100644 --- a/src/ImageSharp/Formats/WebP/WebPEncoder.cs +++ b/src/ImageSharp/Formats/WebP/WebPEncoder.cs @@ -12,6 +12,18 @@ namespace SixLabors.ImageSharp.Formats.WebP /// public sealed class WebPEncoder : IImageEncoder, IWebPEncoderOptions { + /// + public bool Lossless { get; set; } + + /// + public float Quality { get; set; } + + /// + public bool AlphaCompression { get; set; } + + /// + public int EntropyPasses { get; set; } + /// 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 bf437d985..c01984661 100644 --- a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs @@ -1,8 +1,12 @@ // Copyright (c) Six Labors and contributors. // Licensed under the GNU Affero General Public License, Version 3. +using System; +using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.WebP.BitWriter; +using SixLabors.ImageSharp.Formats.WebP.Lossless; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -24,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.WebP /// private Configuration configuration; + /// + /// A bit writer for writing lossless webp streams. + /// + private Vp8LBitWriter bitWriter; + /// /// Initializes a new instance of the class. /// @@ -48,6 +57,218 @@ namespace SixLabors.ImageSharp.Formats.WebP this.configuration = image.GetConfiguration(); ImageMetadata metadata = image.Metadata; + + int width = image.Width; + int height = image.Height; + int initialSize = width * height; + this.bitWriter = new Vp8LBitWriter(initialSize); + + // Write image size. + this.WriteImageSize(width, height); + + // Write the non-trivial Alpha flag and lossless version. + bool hasAlpha = false; // TODO: for the start, this will be always false. + this.WriteRealAlphaAndVersion(hasAlpha); + + // Encode the main image stream. + this.EncodeStream(image); + } + + private void WriteImageSize(int inputImgWidth, int inputImgHeight) + { + Guard.MustBeLessThan(inputImgWidth, WebPConstants.MaxDimension, nameof(inputImgWidth)); + Guard.MustBeLessThan(inputImgHeight, WebPConstants.MaxDimension, nameof(inputImgHeight)); + + uint width = (uint)inputImgWidth - 1; + uint height = (uint)inputImgHeight - 1; + + this.bitWriter.PutBits(width, WebPConstants.Vp8LImageSizeBits); + this.bitWriter.PutBits(height, WebPConstants.Vp8LImageSizeBits); + } + + private void WriteRealAlphaAndVersion(bool hasAlpha) + { + this.bitWriter.PutBits(hasAlpha ? 1U : 0, 1); + this.bitWriter.PutBits(WebPConstants.Vp8LVersion, WebPConstants.Vp8LVersionBits); + } + + private void EncodeStream(Image image) + where TPixel : unmanaged, IPixel + { + var encoder = new Vp8LEncoder(); + + // Analyze image (entropy, num_palettes etc). + this.EncoderAnalyze(image); + } + + /// + /// Analyzes the image and decides what transforms should be used. + /// + private void EncoderAnalyze(Image image) + where TPixel : unmanaged, IPixel + { + // TODO: low effort is always false for now. + bool lowEffort = false; + + // Check if we only deal with a small number of colors and should use a palette. + var usePalette = this.AnalyzeAndCreatePalette(image, lowEffort); + } + + /// + /// If number of colors in the image is less than or equal to MAX_PALETTE_SIZE, + /// creates a palette and returns true, else returns false. + /// + /// true, if a palette should be used. + private bool AnalyzeAndCreatePalette(Image image, bool lowEffort) + where TPixel : unmanaged, IPixel + { + int numColors = this.GetColorPalette(image, out uint[] palette); + + if (numColors > WebPConstants.MaxPaletteSize) + { + return false; + } + + // TODO: figure out how the palette needs to be sorted. + Array.Sort(palette); + + if (!lowEffort && PaletteHasNonMonotonousDeltas(palette, numColors)) + { + GreedyMinimizeDeltas(palette, numColors); + } + + return true; + } + + private int GetColorPalette(Image image, out uint[] palette) + where TPixel : unmanaged, IPixel + { + Rgba32 color = default; + palette = null; + var colors = new HashSet(); + for (int y = 0; y < image.Height; y++) + { + System.Span rowSpan = image.GetPixelRowSpan(y); + for (int x = 0; x < rowSpan.Length; x++) + { + colors.Add(rowSpan[x]); + if (colors.Count > WebPConstants.MaxPaletteSize) + { + // Exact count is not needed, because a palette will not be used then anyway. + return WebPConstants.MaxPaletteSize + 1; + } + } + } + + // Fill the colors into the palette. + palette = new uint[colors.Count]; + using HashSet.Enumerator colorEnumerator = colors.GetEnumerator(); + int idx = 0; + while (colorEnumerator.MoveNext()) + { + colorEnumerator.Current.ToRgba32(ref color); + var bgra = new Bgra32(color.R, color.G, color.B, color.A); + palette[idx++] = bgra.PackedValue; + } + + return colors.Count; + } + + /// + /// The palette has been sorted by alpha. This function checks if the other components of the palette + /// have a monotonic development with regards to position in the palette. + /// If all have monotonic development, there is no benefit to re-organize them greedily. A monotonic development + /// would be spotted in green-only situations (like lossy alpha) or gray-scale images. + /// + /// The palette. + /// Number of colors in the palette. + /// True, if the palette has no monotonous deltas. + private static bool PaletteHasNonMonotonousDeltas(uint[] palette, int numColors) + { + uint predict = 0x000000; + byte signFound = 0x00; + for (int i = 0; i < numColors; ++i) + { + uint diff = LosslessUtils.SubPixels(palette[i], predict); + byte rd = (byte)((diff >> 16) & 0xff); + byte gd = (byte)((diff >> 8) & 0xff); + byte bd = (byte)((diff >> 0) & 0xff); + if (rd != 0x00) + { + signFound |= (byte)((rd < 0x80) ? 1 : 2); + } + + if (gd != 0x00) + { + signFound |= (byte)((gd < 0x80) ? 8 : 16); + } + + if (bd != 0x00) + { + signFound |= (byte)((bd < 0x80) ? 64 : 128); + } + } + + return (signFound & (signFound << 1)) != 0; // two consequent signs. + } + + /// + /// Find greedily always the closest color of the predicted color to minimize + /// deltas in the palette. This reduces storage needs since the palette is stored with delta encoding. + /// + /// The palette. + /// The number of colors in the palette. + private static void GreedyMinimizeDeltas(uint[] palette, int numColors) + { + uint predict = 0x00000000; + for (int i = 0; i < numColors; ++i) + { + int bestIdx = i; + uint bestScore = ~0U; + for (int k = i; k < numColors; ++k) + { + uint curScore = PaletteColorDistance(palette[k], predict); + if (bestScore > curScore) + { + bestScore = curScore; + bestIdx = k; + } + } + + // swap color(palette[bestIdx], palette[i]); + uint best = palette[bestIdx]; + palette[bestIdx] = palette[i]; + palette[i] = best; + predict = palette[i]; + } + } + + /// + /// Computes a value that is related to the entropy created by the + /// palette entry diff. + /// + /// Note that the last & 0xff is a no-operation in the next statement, but + /// removed by most compilers and is here only for regularity of the code. + /// + /// First color. + /// Second color. + /// The color distance. + private static uint PaletteColorDistance(uint col1, uint col2) + { + uint diff = LosslessUtils.SubPixels(col1, col2); + int moreWeightForRGBThanForAlpha = 9; + uint score = PaletteComponentDistance((diff >> 0) & 0xff); + score += PaletteComponentDistance((diff >> 8) & 0xff); + score += PaletteComponentDistance((diff >> 16) & 0xff); + score *= moreWeightForRGBThanForAlpha; + score += PaletteComponentDistance((diff >> 24) & 0xff); + + return score; + } + + private static uint PaletteComponentDistance(uint v) + { + return (v <= 128) ? v : (256 - v); } } }