From 023a64ce1287c99994f7c233d69596719b6f6a3e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 19 Jan 2020 18:23:16 +0100 Subject: [PATCH] WIP: Parse further VP8 header: SegmentHeader, FilterHeader, implement BitReader for VP8 (unfinished) --- src/ImageSharp/Formats/WebP/BitReaderBase.cs | 13 -- src/ImageSharp/Formats/WebP/Vp8BitReader.cs | 127 ++++++++++++++++-- .../Formats/WebP/Vp8FilterHeader.cs | 35 +++++ src/ImageSharp/Formats/WebP/Vp8LBitReader.cs | 34 ++++- .../Formats/WebP/Vp8PictureHeader.cs | 6 +- .../Formats/WebP/Vp8SegmentHeader.cs | 21 ++- .../Formats/WebP/WebPDecoderCore.cs | 110 +++++++++++++-- src/ImageSharp/Formats/WebP/WebPImageInfo.cs | 26 +++- 8 files changed, 324 insertions(+), 48 deletions(-) create mode 100644 src/ImageSharp/Formats/WebP/Vp8FilterHeader.cs diff --git a/src/ImageSharp/Formats/WebP/BitReaderBase.cs b/src/ImageSharp/Formats/WebP/BitReaderBase.cs index a07c68ed94..0a62299eac 100644 --- a/src/ImageSharp/Formats/WebP/BitReaderBase.cs +++ b/src/ImageSharp/Formats/WebP/BitReaderBase.cs @@ -19,19 +19,6 @@ namespace SixLabors.ImageSharp.Formats.WebP /// protected byte[] Data { get; private set; } - /// - /// Reads a single bit from the stream. - /// - /// True if the bit read was 1, false otherwise. - public abstract bool ReadBit(); - - /// - /// Reads a unsigned short value from the inputStream. The bits of each byte are read in least-significant-bit-first order. - /// - /// The number of bits to read (should not exceed 16). - /// A ushort value. - public abstract uint ReadValue(int nBits); - /// /// Copies the raw encoded image data from the stream into a byte array. /// diff --git a/src/ImageSharp/Formats/WebP/Vp8BitReader.cs b/src/ImageSharp/Formats/WebP/Vp8BitReader.cs index cac683b1f4..a3996b08da 100644 --- a/src/ImageSharp/Formats/WebP/Vp8BitReader.cs +++ b/src/ImageSharp/Formats/WebP/Vp8BitReader.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers.Binary; using System.IO; using SixLabors.Memory; @@ -13,15 +14,17 @@ namespace SixLabors.ImageSharp.Formats.WebP /// internal class Vp8BitReader : BitReaderBase { + private const int BitsCount = 56; + /// /// Current value. /// - private long value; + private ulong value; /// /// Current range minus 1. In [127, 254] interval. /// - private int range; + private uint range; /// /// Number of valid bits left. @@ -36,18 +39,23 @@ namespace SixLabors.ImageSharp.Formats.WebP /// /// End of read buffer. /// - private byte bufEnd; + // private byte bufEnd; /// /// Max packed-read position on buffer. /// - private byte bufMax; + // private byte bufMax; /// /// True if input is exhausted. /// private bool eof; + /// + /// Byte position in buffer. + /// + private long pos; + /// /// Initializes a new instance of the class. /// @@ -57,27 +65,126 @@ namespace SixLabors.ImageSharp.Formats.WebP public Vp8BitReader(Stream inputStream, uint imageDataSize, MemoryAllocator memoryAllocator) { this.ReadImageDataFromStream(inputStream, (int)imageDataSize, memoryAllocator); + + this.range = 255 - 1; + this.value = 0; + this.bits = -8; // to load the very first 8bits; + this.eof = false; + this.pos = 0; + + this.LoadNewBytes(); } - /// - public override bool ReadBit() + public int GetBit(int prob) { - throw new NotImplementedException(); + Guard.MustBeGreaterThan(prob, 0, nameof(prob)); + + uint range = this.range; + if (this.bits < 0) + { + this.LoadNewBytes(); + } + + int pos = this.bits; + uint split = (uint)((range * prob) >> 8); + bool bit = this.value > split; + if (bit) + { + range -= split; + this.value -= (ulong)(split + 1) << pos; + } + else + { + range = split + 1; + } + + int shift = 7 ^ this.BitsLog2Floor(range); + range <<= shift; + this.bits -= shift; + + this.range = range - 1; + + return bit ? 1 : 0; + } + + public bool ReadBool() + { + return this.ReadValue(1) is 1; } - /// - public override uint ReadValue(int nBits) + public uint ReadValue(int nBits) { Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); - throw new NotImplementedException(); + uint v = 0; + while (this.bits-- > 0) + { + v |= (uint)this.GetBit(0x80) << this.bits; + } + + return v; } public int ReadSignedValue(int nBits) { Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + int value = (int)this.ReadValue(nBits); + return this.ReadValue(1) != 0 ? -value : value; + } + + private void LoadNewBytes() + { + if (this.pos < this.Data.Length) + { + ulong bits; + ulong inBits = BinaryPrimitives.ReadUInt64LittleEndian(this.Data.AsSpan().Slice((int)this.pos, 8)); + this.pos += BitsCount >> 3; + this.buf = this.Data[BitsCount >> 3]; + bits = this.ByteSwap64(inBits); + bits >>= 64 - BitsCount; + this.value = bits | (this.value << BitsCount); + this.bits += BitsCount; + } + else + { + this.LoadFinalBytes(); + } + } + + private void LoadFinalBytes() + { + // Only read 8bits at a time. + if (this.pos < this.Data.Length) + { + this.bits += 8; + this.value = this.Data[this.pos++] | (this.value << 8); + } + else if (!this.eof) + { + this.value <<= 8; + this.bits += 8; + this.eof = true; + } + else + { + this.bits = 0; // This is to avoid undefined behaviour with shifts. + } + } + + private ulong ByteSwap64(ulong x) + { + x = ((x & 0xffffffff00000000ul) >> 32) | ((x & 0x00000000fffffffful) << 32); + x = ((x & 0xffff0000ffff0000ul) >> 16) | ((x & 0x0000ffff0000fffful) << 16); + x = ((x & 0xff00ff00ff00ff00ul) >> 8) | ((x & 0x00ff00ff00ff00fful) << 8); + return x; + } + + private int BitsLog2Floor(uint n) + { + long firstSetBit; throw new NotImplementedException(); + // BitScanReverse(firstSetBit, n); } } } diff --git a/src/ImageSharp/Formats/WebP/Vp8FilterHeader.cs b/src/ImageSharp/Formats/WebP/Vp8FilterHeader.cs new file mode 100644 index 0000000000..7093b1bf0a --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8FilterHeader.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal class Vp8FilterHeader + { + private const int NumRefLfDeltas = 4; + + private const int NumModeLfDeltas = 4; + + public Vp8FilterHeader() + { + this.RefLfDelta = new int[NumRefLfDeltas]; + this.ModeLfDelta = new int[NumModeLfDeltas]; + } + + /// + /// Gets or sets the loop filter. + /// + public LoopFilter LoopFilter { get; set; } + + // [0..63] + public int Level { get; set; } + + // [0..7] + public int Sharpness { get; set; } + + public bool UseLfDelta { get; set; } + + public int[] RefLfDelta { get; private set; } + + public int[] ModeLfDelta { get; private set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs b/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs index 27dab46878..8938124728 100644 --- a/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs +++ b/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs @@ -95,8 +95,12 @@ namespace SixLabors.ImageSharp.Formats.WebP this.pos = length; } - /// - public override uint ReadValue(int nBits) + /// + /// Reads a unsigned short value from the buffer. The bits of each byte are read in least-significant-bit-first order. + /// + /// The number of bits to read (should not exceed 16). + /// A ushort value. + public uint ReadValue(int nBits) { Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); @@ -113,23 +117,37 @@ namespace SixLabors.ImageSharp.Formats.WebP return 0; } - /// - public override bool ReadBit() + /// + /// Reads a single bit from the stream. + /// + /// True if the bit read was 1, false otherwise. + public bool ReadBit() { uint bit = this.ReadValue(1); return bit != 0; } - public void AdvanceBitPosition(int bitPosition) + /// + /// For jumping over a number of bits in the bit stream when accessed with PrefetchBits and FillBitWindow. + /// + /// The number of bits to advance the position. + public void AdvanceBitPosition(int numberOfBits) { - this.bitPos += bitPosition; + this.bitPos += numberOfBits; } + /// + /// Return the pre-fetched bits, so they can be looked up. + /// + /// Prefetched bits. public ulong PrefetchBits() { return this.value >> (this.bitPos & (Vp8LLbits - 1)); } + /// + /// Advances the read buffer by 4 bytes to make room for reading next 32 bits. + /// public void FillBitWindow() { if (this.bitPos >= Vp8LWbits) @@ -138,6 +156,10 @@ namespace SixLabors.ImageSharp.Formats.WebP } } + /// + /// Returns true if there was an attempt at reading bit past the end of the buffer. + /// + /// True, if end of buffer was reached. public bool IsEndOfStream() { return this.eos || ((this.pos == this.len) && (this.bitPos > Vp8LLbits)); diff --git a/src/ImageSharp/Formats/WebP/Vp8PictureHeader.cs b/src/ImageSharp/Formats/WebP/Vp8PictureHeader.cs index f9f0fd37d2..3f80b13b29 100644 --- a/src/ImageSharp/Formats/WebP/Vp8PictureHeader.cs +++ b/src/ImageSharp/Formats/WebP/Vp8PictureHeader.cs @@ -26,12 +26,16 @@ namespace SixLabors.ImageSharp.Formats.WebP public sbyte YScale { get; set; } /// - /// Gets or sets the colorspace. 0 = YCbCr. + /// Gets or sets the colorspace. + /// 0 - YUV color space similar to the YCrCb color space defined in. + /// 1 - Reserved for future use. /// public sbyte ColorSpace { get; set; } /// /// Gets or sets the clamp type. + /// 0 - Decoders are required to clamp the reconstructed pixel values to between 0 and 255 (inclusive). + /// 1 - Reconstructed pixel values are guaranteed to be between 0 and 255; no clamping is necessary. /// public sbyte ClampType { get; set; } } diff --git a/src/ImageSharp/Formats/WebP/Vp8SegmentHeader.cs b/src/ImageSharp/Formats/WebP/Vp8SegmentHeader.cs index 993b744845..27bce1f043 100644 --- a/src/ImageSharp/Formats/WebP/Vp8SegmentHeader.cs +++ b/src/ImageSharp/Formats/WebP/Vp8SegmentHeader.cs @@ -8,6 +8,14 @@ namespace SixLabors.ImageSharp.Formats.WebP /// internal class Vp8SegmentHeader { + private const int NumMbSegments = 4; + + public Vp8SegmentHeader() + { + this.Quantizer = new byte[NumMbSegments]; + this.FilterStrength = new byte[NumMbSegments]; + } + public bool UseSegment { get; set; } /// @@ -16,18 +24,19 @@ namespace SixLabors.ImageSharp.Formats.WebP public bool UpdateMap { get; set; } /// - /// Gets or sets the absolute or delta values for quantizer and filter. + /// Gets or sets a value indicating whether to use delta values for quantizer and filter. + /// If this value is false, absolute values are used. /// - public int AbsoluteOrDelta { get; set; } + public bool Delta { get; set; } /// - /// Gets or sets quantization changes. + /// Gets quantization changes. /// - public byte[] Quantizer { get; set; } + public byte[] Quantizer { get; private set; } /// - /// Gets or sets the filter strength for segments. + /// Gets the filter strength for segments. /// - public byte[] FilterStrength { get; set; } + public byte[] FilterStrength { get; private set; } } } diff --git a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs index 1816e56ae0..f0b0233cf5 100644 --- a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs @@ -85,7 +85,7 @@ namespace SixLabors.ImageSharp.Formats.WebP WebPThrowHelper.ThrowNotSupportedException("Animations are not supported"); } - var image = new Image(this.configuration, imageInfo.Width, imageInfo.Height, this.Metadata); + var image = new Image(this.configuration, (int)imageInfo.Width, (int)imageInfo.Height, this.Metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); if (imageInfo.IsLossLess) { @@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp.Formats.WebP this.ReadImageHeader(); WebPImageInfo imageInfo = this.ReadVp8Info(); - return new ImageInfo(new PixelTypeInfo((int)imageInfo.BitsPerPixel), imageInfo.Width, imageInfo.Height, this.Metadata); + return new ImageInfo(new PixelTypeInfo((int)imageInfo.BitsPerPixel), (int)imageInfo.Width, (int)imageInfo.Height, this.Metadata); } /// @@ -200,12 +200,12 @@ namespace SixLabors.ImageSharp.Formats.WebP // 3 bytes for the width. this.currentStream.Read(this.buffer, 0, 3); this.buffer[3] = 0; - int width = BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; + uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; // 3 bytes for the height. this.currentStream.Read(this.buffer, 0, 3); this.buffer[3] = 0; - int height = BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; + uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; // Optional chunks ICCP, ALPH and ANIM can follow here. WebPChunkType chunkType; @@ -288,7 +288,7 @@ namespace SixLabors.ImageSharp.Formats.WebP this.currentStream.Read(this.buffer, 0, 4); uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); - // https://tools.ietf.org/html/rfc6386#page-30 + // See paragraph 9.1 https://tools.ietf.org/html/rfc6386#page-30 // Frame tag that contains four fields: // - A 1-bit frame type (0 for key frames, 1 for interframes). // - A 3-bit version number. @@ -328,15 +328,103 @@ namespace SixLabors.ImageSharp.Formats.WebP } this.currentStream.Read(this.buffer, 0, 4); - int width = BinaryPrimitives.ReadInt16LittleEndian(this.buffer) & 0x3fff; - int height = BinaryPrimitives.ReadInt16LittleEndian(this.buffer.AsSpan(2)) & 0x3fff; + uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer); + uint width = tmp & 0x3fff; + sbyte xScale = (sbyte)(tmp >> 6); + tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer.AsSpan(2)); + uint height = tmp & 0x3fff; + sbyte yScale = (sbyte)(tmp >> 6); if (width is 0 || height is 0) { WebPThrowHelper.ThrowImageFormatException("width or height can not be zero"); } + var vp8FrameHeader = new Vp8FrameHeader() + { + KeyFrame = true, + Profile = (sbyte)version, + PartitionLength = partitionLength + }; + var bitReader = new Vp8BitReader(this.currentStream, dataSize - 10, this.memoryAllocator); + // Paragraph 9.2: color space and clamp type follow + sbyte colorSpace = (sbyte)bitReader.ReadValue(1); + sbyte clampType = (sbyte)bitReader.ReadValue(1); + var vp8PictureHeader = new Vp8PictureHeader() + { + Width = width, + Height = height, + XScale = xScale, + YScale = yScale, + ColorSpace = colorSpace, + ClampType = clampType + }; + + // Paragraph 9.3: Parse the segment header. + var vp8SegmentHeader = new Vp8SegmentHeader(); + vp8SegmentHeader.UseSegment = bitReader.ReadBool(); + if (vp8SegmentHeader.UseSegment) + { + vp8SegmentHeader.UpdateMap = bitReader.ReadBool(); + bool updateData = bitReader.ReadBool(); + if (updateData) + { + vp8SegmentHeader.Delta = bitReader.ReadBool(); + for (int i = 0; i < vp8SegmentHeader.Quantizer.Length; i++) + { + bool hasValue = bitReader.ReadBool(); + uint quantizeValue = hasValue ? bitReader.ReadValue(7) : 0; + vp8SegmentHeader.Quantizer[i] = (byte)quantizeValue; + } + + for (int i = 0; i < vp8SegmentHeader.FilterStrength.Length; i++) + { + bool hasValue = bitReader.ReadBool(); + uint filterStrengthValue = hasValue ? bitReader.ReadValue(6) : 0; + vp8SegmentHeader.FilterStrength[i] = (byte)filterStrengthValue; + } + + if (vp8SegmentHeader.UpdateMap) + { + // TODO: Read VP8Proba + } + } + } + + // Paragraph 9.4: Parse the filter specs. + var vp8FilterHeader = new Vp8FilterHeader(); + vp8FilterHeader.LoopFilter = bitReader.ReadBool() ? LoopFilter.Simple : LoopFilter.Normal; + vp8FilterHeader.Level = (int)bitReader.ReadValue(6); + vp8FilterHeader.Sharpness = (int)bitReader.ReadValue(3); + vp8FilterHeader.UseLfDelta = bitReader.ReadBool(); + if (vp8FilterHeader.UseLfDelta) + { + // Update lf-delta? + if (bitReader.ReadBool()) + { + for (int i = 0; i < vp8FilterHeader.RefLfDelta.Length; i++) + { + bool hasValue = bitReader.ReadBool(); + if (hasValue) + { + vp8FilterHeader.RefLfDelta[i] = bitReader.ReadSignedValue(6); + } + } + + for (int i = 0; i < vp8FilterHeader.ModeLfDelta.Length; i++) + { + bool hasValue = bitReader.ReadBool(); + if (hasValue) + { + vp8FilterHeader.ModeLfDelta[i] = bitReader.ReadSignedValue(6); + } + } + } + } + + // TODO: ParsePartitions + return new WebPImageInfo() { Width = width, @@ -345,6 +433,10 @@ namespace SixLabors.ImageSharp.Formats.WebP IsLossLess = false, Features = features, Vp8Profile = (sbyte)version, + Vp8FrameHeader = vp8FrameHeader, + Vp8SegmentHeader = vp8SegmentHeader, + Vp8FilterHeader = vp8FilterHeader, + Vp8PictureHeader = vp8PictureHeader, Vp8BitReader = bitReader }; } @@ -392,8 +484,8 @@ namespace SixLabors.ImageSharp.Formats.WebP return new WebPImageInfo() { - Width = (int)width, - Height = (int)height, + Width = width, + Height = height, BitsPerPixel = WebPBitsPerPixel.Pixel32, IsLossLess = true, Features = features, diff --git a/src/ImageSharp/Formats/WebP/WebPImageInfo.cs b/src/ImageSharp/Formats/WebP/WebPImageInfo.cs index 08bfe2314c..1eeb4d2e0a 100644 --- a/src/ImageSharp/Formats/WebP/WebPImageInfo.cs +++ b/src/ImageSharp/Formats/WebP/WebPImageInfo.cs @@ -8,12 +8,12 @@ namespace SixLabors.ImageSharp.Formats.WebP /// /// Gets or sets the bitmap width in pixels (signed integer). /// - public int Width { get; set; } + public uint Width { get; set; } /// /// Gets or sets the bitmap height in pixels (signed integer). /// - public int Height { get; set; } + public uint Height { get; set; } /// /// Gets or sets the bits per pixel. @@ -36,7 +36,27 @@ namespace SixLabors.ImageSharp.Formats.WebP public int Vp8Profile { get; set; } = -1; /// - /// Gets or sets the Vp8L bitreader. Will be null, if its not lossless image. + /// Gets or sets the VP8 frame header. + /// + public Vp8FrameHeader Vp8FrameHeader { get; set; } + + /// + /// Gets or sets the VP8 picture header. + /// + public Vp8PictureHeader Vp8PictureHeader { get; set; } + + /// + /// Gets or sets the VP8 segment header. + /// + public Vp8SegmentHeader Vp8SegmentHeader { get; set; } + + /// + /// Gets or sets the VP8 filter header. + /// + public Vp8FilterHeader Vp8FilterHeader { get; set; } + + /// + /// Gets or sets the VP8L bitreader. Will be null, if its not lossless image. /// public Vp8LBitReader Vp8LBitReader { get; set; } = null;