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 =
{