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);
}
}
}