diff --git a/src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs index 1b011315c2..73e8e889c9 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs @@ -61,6 +61,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter /// public abstract void Finish(); + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + public abstract void WriteEncodedImageToStream(Stream stream); + protected bool ResizeBuffer(int maxBytes, int sizeRequired) { if (maxBytes > 0 && sizeRequired < maxBytes) @@ -83,59 +89,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter return false; } - /// - /// Writes the encoded image to the stream. - /// - /// If true, lossy tag will be written, otherwise a lossless tag. - /// The stream to write to. - public void WriteEncodedImageToStream(bool lossy, Stream stream) - { - this.Finish(); - var numBytes = this.NumBytes(); - var size = numBytes; - if (!lossy) - { - size++; // One byte extra for the VP8L signature. - } - - var pad = size & 1; - var riffSize = WebPConstants.TagSize + WebPConstants.ChunkHeaderSize + size + pad; - this.WriteRiffHeader(riffSize, size, lossy, stream); - this.WriteToStream(stream); - if (pad == 1) - { - stream.WriteByte(0); - } - } - /// /// Writes the RIFF header to the stream. /// - /// The block length. - /// The size in bytes of the encoded image. - /// If true, lossy tag will be written, otherwise a lossless tag. /// The stream to write to. - private void WriteRiffHeader(int riffSize, int size, bool lossy, Stream stream) + /// The block length. + protected void WriteRiffHeader(Stream stream, uint riffSize) { Span buffer = stackalloc byte[4]; stream.Write(WebPConstants.RiffFourCc); - BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)riffSize); + BinaryPrimitives.WriteUInt32LittleEndian(buffer, riffSize); stream.Write(buffer); stream.Write(WebPConstants.WebPHeader); - - if (lossy) - { - stream.Write(WebPConstants.Vp8MagicBytes); - } - else - { - stream.Write(WebPConstants.Vp8LMagicBytes); - } - - BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)size); - stream.Write(buffer); - stream.WriteByte(WebPConstants.Vp8LMagicByte); } } } diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs index e18a945bcc..89cff21663 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Buffers.Binary; +using System.IO; using SixLabors.ImageSharp.Formats.WebP.Lossy; namespace SixLabors.ImageSharp.Formats.WebP.BitWriter @@ -10,6 +13,27 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter /// internal class Vp8BitWriter : BitWriterBase { +#pragma warning disable SA1310 // Field names should not contain underscore + private const int DC_PRED = 0; + private const int TM_PRED = 1; + private const int V_PRED = 2; + private const int H_PRED = 3; + + // 4x4 modes + private const int B_DC_PRED = 0; + private const int B_TM_PRED = 1; + private const int B_VE_PRED = 2; + private const int B_HE_PRED = 3; + private const int B_RD_PRED = 4; + private const int B_VR_PRED = 5; + private const int B_LD_PRED = 6; + private const int B_VL_PRED = 7; + private const int B_HD_PRED = 8; + private const int B_HU_PRED = 9; +#pragma warning restore SA1310 // Field names should not contain underscore + + private readonly Vp8Encoder enc; + private int range; private int value; @@ -43,6 +67,17 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter this.maxPos = 0; } + /// + /// Initializes a new instance of the class. + /// + /// The expected size in bytes. + /// The Vp8Encoder. + public Vp8BitWriter(int expectedSize, Vp8Encoder enc) + : this(expectedSize) + { + this.enc = enc; + } + /// public override int NumBytes() { @@ -180,6 +215,74 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter this.Flush(); } + public void PutSegment(int s, Span p) + { + if (this.PutBit(s >= 2, p[0])) + { + p = p.Slice(1); + } + + this.PutBit(s & 1, p[1]); + } + + public void PutI16Mode(int mode) + { + if (this.PutBit(mode == TM_PRED || mode == H_PRED, 156)) + { + this.PutBit(mode == TM_PRED, 128); // TM or HE + } + else + { + this.PutBit(mode == V_PRED, 163); // VE or DC + } + } + + public int PutI4Mode(int mode, Span prob) + { + if (this.PutBit(mode != B_DC_PRED, prob[0])) + { + if (this.PutBit(mode != B_TM_PRED, prob[1])) + { + if (this.PutBit(mode != B_VE_PRED, prob[2])) + { + if (!this.PutBit(mode >= B_LD_PRED, prob[3])) + { + if (this.PutBit(mode != B_HE_PRED, prob[4])) + { + this.PutBit(mode != B_RD_PRED, prob[5]); + } + } + else + { + if (this.PutBit(mode != B_LD_PRED, prob[6])) + { + if (this.PutBit(mode != B_VL_PRED, prob[7])) + { + this.PutBit(mode != B_HD_PRED, prob[8]); + } + } + } + } + } + } + + return mode; + } + + public void PutUvMode(int uvMode) + { + // DC_PRED + if (this.PutBit(uvMode != DC_PRED, 142)) + { + // V_PRED + if (this.PutBit(uvMode != V_PRED, 114)) + { + // H_PRED + this.PutBit(uvMode != H_PRED, 183); + } + } + } + private void PutBits(uint value, int nbBits) { for (uint mask = 1u << (nbBits - 1); mask != 0; mask >>= 1) @@ -249,6 +352,24 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter return bit; } + private void PutSignedBits(int value, int nbBits) + { + if (this.PutBitUniform(value != 0 ? 1 : 0) == 0) + { + return; + } + + if (value < 0) + { + var valueToWrite = ((-value) << 1) | 1; + this.PutBits((uint)valueToWrite, nbBits + 1); + } + else + { + this.PutBits((uint)(value << 1), nbBits + 1); + } + } + private void Flush() { int s = 8 + this.nbBits; @@ -286,5 +407,253 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter this.run++; // Delay writing of bytes 0xff, pending eventual carry. } } + + /// + public override void WriteEncodedImageToStream(Stream stream) + { + this.Finish(); + uint numBytes = (uint)this.NumBytes(); + int mbSize = this.enc.Mbw * this.enc.Mbh; + int expectedSize = mbSize * 7 / 8; + + var bitWriterPartZero = new Vp8BitWriter(expectedSize); + + // Partition #0 with header and partition sizes + uint size0 = this.GeneratePartition0(bitWriterPartZero); + + uint vp8Size = WebPConstants.Vp8FrameHeaderSize + size0; + vp8Size += numBytes; + uint pad = vp8Size & 1; + vp8Size += pad; + + // Compute RIFF size + // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. + var riffSize = WebPConstants.TagSize + WebPConstants.ChunkHeaderSize + vp8Size; + + // Emit headers and partition #0 + this.WriteWebPHeaders(stream, size0, vp8Size, riffSize); + bitWriterPartZero.WriteToStream(stream); + + // Write the encoded image to the stream. + this.WriteToStream(stream); + if (pad == 1) + { + stream.WriteByte(0); + } + } + + private uint GeneratePartition0(Vp8BitWriter bitWriter) + { + bitWriter.PutBitUniform(0); // colorspace + bitWriter.PutBitUniform(0); // clamp type + + this.WriteSegmentHeader(bitWriter); + this.WriteFilterHeader(bitWriter); + + bitWriter.PutBits(0, 2); + + this.WriteQuant(bitWriter); + bitWriter.PutBitUniform(0); + this.WriteProbas(bitWriter); + this.CodeIntraModes(bitWriter); + + bitWriter.Finish(); + + return (uint)bitWriter.NumBytes(); + } + + private void WriteSegmentHeader(Vp8BitWriter bitWriter) + { + Vp8EncSegmentHeader hdr = this.enc.SegmentHeader; + Vp8EncProba proba = this.enc.Proba; + if (bitWriter.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) + { + // We always 'update' the quant and filter strength values. + int updateData = 1; + bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0); + if (bitWriter.PutBitUniform(updateData) != 0) + { + // We always use absolute values, not relative ones. + bitWriter.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) + for (int s = 0; s < WebPConstants.NumMbSegments; ++s) + { + bitWriter.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); + } + + for (int s = 0; s < WebPConstants.NumMbSegments; ++s) + { + bitWriter.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); + } + } + + if (hdr.UpdateMap) + { + for (int s = 0; s < 3; ++s) + { + if (bitWriter.PutBitUniform((proba.Segments[s] != 255) ? 1 : 0) != 0) + { + bitWriter.PutBits(proba.Segments[s], 8); + } + } + } + } + } + + private void WriteFilterHeader(Vp8BitWriter bitWriter) + { + Vp8FilterHeader hdr = this.enc.FilterHeader; + var useLfDelta = hdr.I4x4LfDelta != 0; + bitWriter.PutBitUniform(hdr.Simple ? 1 : 0); + bitWriter.PutBits((uint)hdr.FilterLevel, 6); + bitWriter.PutBits((uint)hdr.Sharpness, 3); + if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0) + { + // '0' is the default value for i4x4LfDelta at frame #0. + bool needUpdate = hdr.I4x4LfDelta != 0; + if (bitWriter.PutBitUniform(needUpdate ? 1 : 0) != 0) + { + // we don't use refLfDelta => emit four 0 bits. + bitWriter.PutBits(0, 4); + + // we use modeLfDelta for i4x4 + bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6); + bitWriter.PutBits(0, 3); // all others unused. + } + } + } + + // Nominal quantization parameters + private void WriteQuant(Vp8BitWriter bitWriter) + { + bitWriter.PutBits((uint)this.enc.BaseQuant, 7); + bitWriter.PutSignedBits(this.enc.DqY1Dc, 4); + bitWriter.PutSignedBits(this.enc.DqY2Dc, 4); + bitWriter.PutSignedBits(this.enc.DqY2Ac, 4); + bitWriter.PutSignedBits(this.enc.DqUvDc, 4); + bitWriter.PutSignedBits(this.enc.DqUvAc, 4); + } + + private void WriteProbas(Vp8BitWriter bitWriter) + { + Vp8EncProba probas = this.enc.Proba; + for (int t = 0; t < WebPConstants.NumTypes; ++t) + { + for (int b = 0; b < WebPConstants.NumBands; ++b) + { + for (int c = 0; c < WebPConstants.NumCtx; ++c) + { + for (int p = 0; p < WebPConstants.NumProbas; ++p) + { + byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p]; + bool update = p0 != WebPLookupTables.DefaultCoeffsProba[t, b, c, p]; + if (bitWriter.PutBit(update, WebPLookupTables.CoeffsUpdateProba[t, b, c, p])) + { + bitWriter.PutBits(p0, 8); + } + } + } + } + } + + if (bitWriter.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) + { + bitWriter.PutBits(probas.SkipProba, 8); + } + } + + private void CodeIntraModes(Vp8BitWriter bitWriter) + { + var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.Mbw, this.enc.Mbh); + int predsWidth = this.enc.PredsWidth; + + do + { + Vp8MacroBlockInfo mb = it.CurrentMacroBlockInfo; + Span preds = it.Preds.AsSpan(it.PredIdx); + if (this.enc.SegmentHeader.UpdateMap) + { + bitWriter.PutSegment(mb.Segment, this.enc.Proba.Segments); + } + + if (this.enc.Proba.UseSkipProba) + { + bitWriter.PutBit(mb.Skip, this.enc.Proba.SkipProba); + } + + if (bitWriter.PutBit(mb.MacroBlockType != 0, 145)) + { + // i16x16 + bitWriter.PutI16Mode(preds[0]); + } + else + { + Span topPred = it.Preds.AsSpan(it.PredIdx); + int x, y; + for (y = 0; y < 4; ++y) + { + int left = preds[it.PredIdx - 1]; + for (x = 0; x < 4; ++x) + { + byte[] probas = WebPLookupTables.ModesProba[topPred[x], left]; + left = bitWriter.PutI4Mode(preds[x], probas); + } + + topPred = preds; + preds = preds.Slice(predsWidth); + } + } + + bitWriter.PutUvMode(mb.UvMode); + } + while (it.Next()); + } + + private void WriteWebPHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize) + { + this.WriteRiffHeader(stream, riffSize); + this.WriteVp8Header(stream, vp8Size); + this.WriteFrameHeader(stream, size0); + } + + private void WriteVp8Header(Stream stream, uint size) + { + Span vp8ChunkHeader = stackalloc byte[WebPConstants.ChunkHeaderSize]; + + WebPConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader); + BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader.Slice(4), size); + + stream.Write(vp8ChunkHeader); + } + + private void WriteFrameHeader(Stream stream, uint size0) + { + uint profile = 0; + int width = this.enc.Width; + int height = this.enc.Height; + var vp8FrameHeader = new byte[WebPConstants.Vp8FrameHeaderSize]; + + // Paragraph 9.1. + uint bits = 0 // keyframe (1b) + | (profile << 1) // profile (3b) + | (1 << 4) // visible (1b) + | (size0 << 5); // partition length (19b) + + vp8FrameHeader[0] = (byte)((bits >> 0) & 0xff); + vp8FrameHeader[1] = (byte)((bits >> 8) & 0xff); + vp8FrameHeader[2] = (byte)((bits >> 16) & 0xff); + + // signature + vp8FrameHeader[3] = WebPConstants.Vp8HeaderMagicBytes[0]; + vp8FrameHeader[4] = WebPConstants.Vp8HeaderMagicBytes[1]; + vp8FrameHeader[5] = WebPConstants.Vp8HeaderMagicBytes[2]; + + // dimensions + vp8FrameHeader[6] = (byte)(width & 0xff); + vp8FrameHeader[7] = (byte)(width >> 8); + vp8FrameHeader[8] = (byte)(height & 0xff); + vp8FrameHeader[9] = (byte)(height >> 8); + + stream.Write(vp8FrameHeader); + } } } diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs index 917646bfea..139eebf9a1 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs @@ -3,6 +3,7 @@ using System; using System.Buffers.Binary; +using System.IO; using SixLabors.ImageSharp.Formats.WebP.Lossless; namespace SixLabors.ImageSharp.Formats.WebP.BitWriter @@ -126,6 +127,33 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter this.used = 0; } + /// + public override void WriteEncodedImageToStream(Stream stream) + { + Span buffer = stackalloc byte[4]; + + this.Finish(); + uint size = (uint)this.NumBytes(); + size++; // One byte extra for the VP8L signature. + + // Write RIFF header. + uint pad = size & 1; + uint riffSize = WebPConstants.TagSize + WebPConstants.ChunkHeaderSize + size + pad; + this.WriteRiffHeader(stream, riffSize); + stream.Write(WebPConstants.Vp8LMagicBytes); + + // Write Vp8 Header. + BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); + stream.Write(buffer); + stream.WriteByte(WebPConstants.Vp8LHeaderMagicByte); + + this.WriteToStream(stream); + if (pad == 1) + { + stream.WriteByte(0); + } + } + /// /// Internal function for PutBits flushing 32 bits from the written state. /// diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs index 55e129e7dc..078710486f 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs @@ -186,7 +186,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(lossy: false, stream); + this.bitWriter.WriteEncodedImageToStream(stream); } /// diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs index 6747ff6dbc..8972e270f2 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs @@ -62,6 +62,16 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// private Vp8EncSegmentHeader segmentHeader; + /// + /// The filter header info's. + /// + private Vp8FilterHeader filterHeader; + + /// + /// The segment infos. + /// + private Vp8SegmentInfo[] segmentInfos; + /// /// Contextual macroblock infos. /// @@ -74,6 +84,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy private readonly Vp8RdLevel rdOptLevel; + private int dqY1Dc; + + private int dqY2Dc; + + private int dqY2Ac; + private int dqUvDc; private int dqUvAc; @@ -122,6 +138,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy private const int QMax = 100; + // TODO: filterStrength is hardcoded, should be configurable. + private const int FilterStrength = 60; + /// /// Initializes a new instance of the class. /// @@ -133,6 +152,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// Number of entropy-analysis passes (in [1..10]). public Vp8Encoder(MemoryAllocator memoryAllocator, int width, int height, int quality, int method, int entropyPasses) { + this.Width = width; + this.Height = height; this.memoryAllocator = memoryAllocator; this.quality = quality.Clamp(0, 100); this.method = method.Clamp(0, 6); @@ -167,8 +188,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.mbInfo[i] = new Vp8MacroBlockInfo(); } - this.proba = new Vp8EncProba(); + this.segmentInfos = new Vp8SegmentInfo[4]; + for (int i = 0; i < 4; i++) + { + this.segmentInfos[i] = new Vp8SegmentInfo(); + } + this.filterHeader = new Vp8FilterHeader(); + this.proba = new Vp8EncProba(); this.Preds = new byte[predSize * 2]; // TODO: figure out how much mem we need here. This is too much. this.predsWidth = (4 * this.mbw) + 1; @@ -180,10 +207,52 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.ResetBoundaryPredictions(); // Initialize the bitwriter. - var baseQuant = 36; // TODO: hardCoded for now. - int averageBytesPerMacroBlock = this.averageBytesPerMb[baseQuant >> 4]; + this.BaseQuant = 36; // TODO: hardCoded for now. + int averageBytesPerMacroBlock = this.averageBytesPerMb[this.BaseQuant >> 4]; int expectedSize = this.mbw * this.mbh * averageBytesPerMacroBlock; - this.bitWriter = new Vp8BitWriter(expectedSize); + this.bitWriter = new Vp8BitWriter(expectedSize, this); + } + + public int BaseQuant { get; } + + /// + /// Gets the probabilities. + /// + public Vp8EncProba Proba + { + get => this.proba; + } + + /// + /// Gets the segment features. + /// + public Vp8EncSegmentHeader SegmentHeader + { + get => this.segmentHeader; + } + + /// + /// Gets the segment infos. + /// + public Vp8SegmentInfo[] SegmentInfos + { + get => this.segmentInfos; + } + + /// + /// Gets the macro block info's. + /// + public Vp8MacroBlockInfo[] MbInfo + { + get => this.mbInfo; + } + + /// + /// Gets the filter header. + /// + public Vp8FilterHeader FilterHeader + { + get => this.filterHeader; } /// @@ -191,6 +260,56 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// public int Alpha { get; set; } + /// + /// Gets the width of the image. + /// + public int Width { get; } + + /// + /// Gets the height of the image. + /// + public int Height { get; } + + public int PredsWidth + { + get => this.predsWidth; + } + + public int Mbw + { + get => this.mbw; + } + + public int Mbh + { + get => this.mbh; + } + + public int DqY1Dc + { + get => this.dqY1Dc; + } + + public int DqY2Ac + { + get => this.dqY2Ac; + } + + public int DqY2Dc + { + get => this.dqY2Dc; + } + + public int DqUvAc + { + get => this.dqUvAc; + } + + public int DqUvDc + { + get => this.dqUvDc; + } + /// /// Gets the luma component. /// @@ -209,22 +328,22 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// /// Gets the top luma samples. /// - private byte[] YTop { get; } + public byte[] YTop { get; } /// /// Gets the top u/v samples. U and V are packed into 16 bytes (8 U + 8 V). /// - private byte[] UvTop { get; } + public byte[] UvTop { get; } /// /// Gets the non-zero pattern. /// - private uint[] Nz { get; } + public uint[] Nz { get; } /// /// Gets the prediction modes: (4*mbw+1) * (4*mbh+1). /// - private byte[] Preds { get; } + public byte[] Preds { get; } /// /// Gets a rough limit for header bits per MB. @@ -249,11 +368,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy int yStride = width; int uvStride = (yStride + 1) >> 1; - var segmentInfos = new Vp8SegmentInfo[4]; - for (int i = 0; i < 4; i++) - { - segmentInfos[i] = new Vp8SegmentInfo(); - } var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.mbInfo, this.Preds, this.mbw, this.mbh); var alphas = new int[WebPConstants.MaxAlpha + 1]; @@ -261,19 +375,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy // Analysis is done, proceed to actual encoding. this.segmentHeader = new Vp8EncSegmentHeader(4); - this.AssignSegments(segmentInfos, alphas); - this.SetLoopParams(segmentInfos, this.quality); + this.AssignSegments(alphas); + this.SetLoopParams(this.quality); // TODO: EncodeAlpha(); // Stats-collection loop. - this.StatLoop(width, height, yStride, uvStride, segmentInfos); + this.StatLoop(width, height, yStride, uvStride); it.Init(); it.InitFilter(); do { var info = new Vp8ModeScore(); it.Import(y, u, v, yStride, uvStride, width, height, false); - if (!this.Decimate(it, segmentInfos, info, this.rdOptLevel)) + if (!this.Decimate(it, info, this.rdOptLevel)) { this.CodeResiduals(it, info); } @@ -286,8 +400,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } while (it.Next()); + // Store filter stats. + this.AdjustFilterStrength(); + // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(lossy: true, stream); + this.bitWriter.WriteEncodedImageToStream(stream); } /// @@ -303,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// This is used for deciding optimal probabilities. It also modifies the /// quantizer value if some target (size, PSNR) was specified. /// - private void StatLoop(int width, int height, int yStride, int uvStride, Vp8SegmentInfo[] segmentInfos) + private void StatLoop(int width, int height, int yStride, int uvStride) { int targetSize = 0; // TODO: target size is hardcoded. float targetPsnr = 0.0f; // TODO: targetPsnr is hardcoded. @@ -334,7 +451,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy while (numPassLeft-- > 0) { bool isLastPass = (MathF.Abs(stats.Dq) <= DqLimit) || (numPassLeft == 0) || (this.maxI4HeaderBits == 0); - var sizeP0 = this.OneStatPass(width, height, yStride, uvStride, rdOpt, nbMbs, stats, segmentInfos); + var sizeP0 = this.OneStatPass(width, height, yStride, uvStride, rdOpt, nbMbs, stats); if (sizeP0 == 0) { return; @@ -373,23 +490,23 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.proba.CalculateLevelCosts(); // finalize costs } - private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8RdLevel rdOpt, int nbMbs, PassStats stats, Vp8SegmentInfo[] segmentInfos) + private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8RdLevel rdOpt, int nbMbs, PassStats stats) { Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.mbInfo, this.Preds, this.mbw, this.mbh); + var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.mbInfo, this.Preds, this.Mbw, this.Mbh); long size = 0; long sizeP0 = 0; long distortion = 0; long pixelCount = nbMbs * 384; - this.SetLoopParams(segmentInfos, stats.Q); + this.SetLoopParams(stats.Q); do { var info = new Vp8ModeScore(); it.Import(y, u, v, yStride, uvStride, width, height, false); - if (this.Decimate(it, segmentInfos, info, rdOpt)) + if (this.Decimate(it, info, rdOpt)) { // Just record the number of skips and act like skipProba is not used. ++this.proba.NbSkip; @@ -420,10 +537,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy return sizeP0; } - private void SetLoopParams(Vp8SegmentInfo[] dqm, float q) + private void SetLoopParams(float q) { // Setup segment quantizations and filters. - this.SetSegmentParams(dqm, q); + this.SetSegmentParams(q); // Compute segment probabilities. this.SetSegmentProbas(); @@ -431,6 +548,33 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.ResetStats(); } + private void AdjustFilterStrength() + { + if (FilterStrength > 0) + { + int maxLevel = 0; + for (int s = 0; s < WebPConstants.NumMbSegments; s++) + { + Vp8SegmentInfo dqm = this.SegmentInfos[s]; + + // this '>> 3' accounts for some inverse WHT scaling + int delta = (dqm.MaxEdge * dqm.Y2.Q[1]) >> 3; + int level = this.FilterStrengthFromDelta(this.filterHeader.Sharpness, delta); + if (level > dqm.FStrength) + { + dqm.FStrength = level; + } + + if (maxLevel < dqm.FStrength) + { + maxLevel = dqm.FStrength; + } + } + + this.filterHeader.FilterLevel = maxLevel; + } + } + private void ResetBoundaryPredictions() { Span top = this.Preds.AsSpan(); // original source top starts at: enc->preds_ - enc->preds_w_ @@ -449,16 +593,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } // Simplified k-Means, to assign Nb segments based on alpha-histogram. - private void AssignSegments(Vp8SegmentInfo[] dqm, int[] alphas) + private void AssignSegments(int[] alphas) { int nb = (this.segmentHeader.NumSegments < NumMbSegments) ? this.segmentHeader.NumSegments : NumMbSegments; var centers = new int[NumMbSegments]; int weightedAverage = 0; var map = new int[WebPConstants.MaxAlpha + 1]; int a, n, k; - int minA; - int maxA; - int rangeA; var accum = new int[NumMbSegments]; var distAccum = new int[NumMbSegments]; @@ -467,13 +608,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy { } - minA = n; + var minA = n; for (n = WebPConstants.MaxAlpha; n > minA && alphas[n] == 0; --n) { } - maxA = n; - rangeA = maxA - minA; + var maxA = n; + var rangeA = maxA - minA; // Spread initial centers evenly. for (k = 0, n = 1; k < nb; ++k, n += 2) @@ -542,12 +683,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } // TODO: add possibility for SmoothSegmentMap - this.SetSegmentAlphas(dqm, centers, weightedAverage); + this.SetSegmentAlphas(centers, weightedAverage); } - private void SetSegmentAlphas(Vp8SegmentInfo[] dqm, int[] centers, int mid) + private void SetSegmentAlphas(int[] centers, int mid) { int nb = this.segmentHeader.NumSegments; + Vp8SegmentInfo[] dqm = this.segmentInfos; int min = centers[0], max = centers[0]; int n; @@ -581,9 +723,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } } - private void SetSegmentParams(Vp8SegmentInfo[] dqm, float quality) + private void SetSegmentParams(float quality) { int nb = this.segmentHeader.NumSegments; + Vp8SegmentInfo[] dqm = this.SegmentInfos; int snsStrength = 50; // TODO: Spatial Noise Shaping, hardcoded for now. double amp = WebPConstants.SnsToDq * snsStrength / 100.0d / 128.0d; double cBase = QualityToCompression(quality / 100.0d); @@ -614,9 +757,42 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.dqUvDc = -4 * snsStrength / 100; this.dqUvDc = Clip(this.dqUvDc, -15, 15); // 4bit-signed max allowed + this.dqY1Dc = 0; + this.dqY2Dc = 0; + this.dqY2Ac = 0; + + // Initialize segments' filtering + this.SetupFilterStrength(); + this.SetupMatrices(dqm); } + private void SetupFilterStrength() + { + var filterSharpness = 0; // TODO: filterSharpness is hardcoded + var filterType = 1; // TODO: filterType is hardcoded + + // level0 is in [0..500]. Using '-f 50' as filter_strength is mid-filtering. + int level0 = 5 * FilterStrength; + for (int i = 0; i < WebPConstants.NumMbSegments; ++i) + { + Vp8SegmentInfo m = this.SegmentInfos[i]; + + // We focus on the quantization of AC coeffs. + int qstep = WebPLookupTables.AcTable[Clip(m.Quant, 0, 127)] >> 2; + int baseStrength = this.FilterStrengthFromDelta(this.filterHeader.Sharpness, qstep); + + // Segments with lower complexity ('beta') will be less filtered. + int f = baseStrength * level0 / (256 + m.Beta); + m.FStrength = (f < WebPConstants.FilterStrengthCutoff) ? 0 : (f > 63) ? 63 : f; + } + + // We record the initial strength (mainly for the case of 1-segment only). + this.filterHeader.FilterLevel = this.SegmentInfos[0].FStrength; + this.filterHeader.Simple = filterType == 0; + this.filterHeader.Sharpness = filterSharpness; + } + private void SetSegmentProbas() { var p = new int[NumMbSegments]; @@ -745,7 +921,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy return bestAlpha; // Mixed susceptibility (not just luma). } - private bool Decimate(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd, Vp8RdLevel rdOpt) + private bool Decimate(Vp8EncIterator it, Vp8ModeScore rd, Vp8RdLevel rdOpt) { rd.InitScore(); @@ -757,7 +933,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy // For method >= 2, pick the best intra4/intra16 based on SSE (~tad slower). // For method <= 1, we don't re-examine the decision but just go ahead with // quantization/reconstruction. - this.RefineUsingDistortion(it, segmentInfos, rd, this.method >= 2, this.method >= 1); + this.RefineUsingDistortion(it, rd, this.method >= 2, this.method >= 1); bool isSkipped = rd.Nz == 0; it.SetSkip(isSkipped); @@ -766,13 +942,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } // Refine intra16/intra4 sub-modes based on distortion only (not rate). - private void RefineUsingDistortion(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd, bool tryBothModes, bool refineUvMode) + private void RefineUsingDistortion(Vp8EncIterator it, Vp8ModeScore rd, bool tryBothModes, bool refineUvMode) { long bestScore = Vp8ModeScore.MaxCost; int nz = 0; int mode; bool isI16 = tryBothModes || (it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16); - Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + Vp8SegmentInfo dqm = this.segmentInfos[it.CurrentMacroBlockInfo.Segment]; // Some empiric constants, of approximate order of magnitude. int lambdaDi16 = 106; @@ -1280,10 +1456,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy int b = dc - tmp[8]; int c = Mul(tmp[4], KC2) - Mul(tmp[12], KC1); int d = Mul(tmp[4], KC1) + Mul(tmp[12], KC2); - Store(dst, reference, 0, i, (a + d)); - Store(dst, reference, 1, i, (b + c)); - Store(dst, reference, 2, i, (b - c)); - Store(dst, reference, 3, i, (a - d)); + Store(dst, reference, 0, i, a + d); + Store(dst, reference, 1, i, b + c); + Store(dst, reference, 2, i, b - c); + Store(dst, reference, 3, i, a - d); tmp = tmp.Slice(1); } } @@ -1663,6 +1839,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy return v; } + private int FilterStrengthFromDelta(int sharpness, int delta) + { + int pos = (delta < WebPConstants.MaxDelzaSize) ? delta : WebPConstants.MaxDelzaSize - 1; + return WebPLookupTables.LevelsFromDelta[sharpness, pos]; + } + [MethodImpl(InliningOptions.ShortMethod)] private static double GetPsnr(long mse, long size) { diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8FilterHeader.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8FilterHeader.cs index 4f5cad659d..4c314e2fcb 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8FilterHeader.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8FilterHeader.cs @@ -53,6 +53,16 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } } + /// + /// Gets or sets a value indicating whether the filtering type is: 0=complex, 1=simple. + /// + public bool Simple { get; set; } + + /// + /// Gets or sets delta filter level for i4x4 relative to i16x16. + /// + public int I4x4LfDelta { get; set; } + public bool UseLfDelta { get; set; } public int[] RefLfDelta { get; } diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs index 7b61997487..7f96b4dba3 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs @@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } this.RecordStats(1, s, 1); - var bit = 2u < (uint)(v + 1); + var bit = (uint)(v + 1) > 2u; if (this.RecordStats(bit ? 1 : 0, s, 2) == 0) { // v = -1 or 1 diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8SegmentHeader.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8SegmentHeader.cs index 3c399fe2e3..1eb144486c 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8SegmentHeader.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8SegmentHeader.cs @@ -10,6 +10,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy { private const int NumMbSegments = 4; + /// + /// Initializes a new instance of the class. + /// public Vp8SegmentHeader() { this.Quantizer = new byte[NumMbSegments]; diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs index 08dac5bf0e..33145e85db 100644 --- a/src/ImageSharp/Formats/WebP/WebPConstants.cs +++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs @@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Formats.WebP /// /// Signature which identifies a VP8 header. /// - public static readonly byte[] Vp8MagicBytes = + public static readonly byte[] Vp8HeaderMagicBytes = { 0x9D, 0x01, @@ -33,10 +33,21 @@ namespace SixLabors.ImageSharp.Formats.WebP /// /// Signature byte which identifies a VP8L header. /// - public const byte Vp8LMagicByte = 0x2F; + public const byte Vp8LHeaderMagicByte = 0x2F; + + /// + /// Signature bytes identifying a lossy image. + /// + public static readonly byte[] Vp8MagicBytes = + { + 0x56, // V + 0x50, // P + 0x38, // 8 + 0x20 // ' ' + }; /// - /// Header bytes identifying a lossless image. + /// Signature bytes identifying a lossless image. /// public static readonly byte[] Vp8LMagicBytes = { @@ -251,6 +262,14 @@ namespace SixLabors.ImageSharp.Formats.WebP public const int QFix = 17; + public const int MaxDelzaSize = 64; + + /// + /// Very small filter-strength values have close to no visual effect. So we can + /// save a little decoding-CPU by turning filtering off for these. + /// + public const int FilterStrengthCutoff = 2; + /// /// Max size of mode partition. /// diff --git a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs index d5bcf1d7af..4270e9efcb 100644 --- a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs @@ -307,7 +307,7 @@ namespace SixLabors.ImageSharp.Formats.WebP // Check for VP8 magic bytes. this.currentStream.Read(this.buffer, 0, 3); - if (!this.buffer.AsSpan().Slice(0, 3).SequenceEqual(WebPConstants.Vp8MagicBytes)) + if (!this.buffer.AsSpan().Slice(0, 3).SequenceEqual(WebPConstants.Vp8HeaderMagicBytes)) { WebPThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); } @@ -341,8 +341,10 @@ namespace SixLabors.ImageSharp.Formats.WebP this.currentStream, remaining, this.memoryAllocator, - partitionLength); - bitReader.Remaining = remaining; + partitionLength) + { + Remaining = remaining + }; return new WebPImageInfo() { @@ -375,7 +377,7 @@ namespace SixLabors.ImageSharp.Formats.WebP // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); - if (signature != WebPConstants.Vp8LMagicByte) + if (signature != WebPConstants.Vp8LHeaderMagicByte) { WebPThrowHelper.ThrowImageFormatException("Invalid VP8L signature"); } diff --git a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs index a550903e09..ed84c377c6 100644 --- a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs +++ b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs @@ -22,6 +22,8 @@ namespace SixLabors.ImageSharp.Formats.WebP public static readonly int[] LinearToGammaTab = new int[WebPConstants.GammaTabSize + 1]; + public static readonly short[,][] Vp8FixedCostsI4 = new short[10, 10][]; + // Compute susceptibility based on DCT-coeff histograms: // the higher, the "easier" the macroblock is to compress. public static readonly int[] Vp8DspScan = @@ -51,7 +53,59 @@ namespace SixLabors.ImageSharp.Formats.WebP 8 + (0 * WebPConstants.Bps), 12 + (0 * WebPConstants.Bps), 8 + (4 * WebPConstants.Bps), 12 + (4 * WebPConstants.Bps) // V }; - public static readonly short[,][] Vp8FixedCostsI4 = new short[10, 10][]; + // This table gives, for a given sharpness, the filtering strength to be + // used (at least) in order to filter a given edge step delta. + public static readonly byte[,] LevelsFromDelta = + { + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, + 20, 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, + 44, 45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 19, + 20, 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, + 44, 46, 47, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16, 18, 19, + 21, 22, 24, 25, 27, 28, 30, 31, 33, 34, 36, 37, 39, 40, 42, 43, + 45, 46, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, + 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, + 45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 13, 15, 16, 17, 19, 20, + 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, 44, + 46, 47, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 13, 15, 16, 18, 19, 21, + 22, 24, 25, 27, 28, 30, 31, 33, 34, 36, 37, 39, 40, 42, 43, 45, + 46, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21, + 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, + 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + } + }; public static readonly byte[] Norm = {