From 6f37d5c873cfd1c8f067fc8006c598f48f74d734 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 17 Jan 2020 12:17:59 +0100 Subject: [PATCH] Introduce bitreader base class --- src/ImageSharp/Formats/WebP/BitReaderBase.cs | 63 ++++++++++++++++++ src/ImageSharp/Formats/WebP/Vp8BitReader.cs | 40 +++++++++-- src/ImageSharp/Formats/WebP/Vp8LBitReader.cs | 66 +++++-------------- .../Formats/WebP/WebPDecoderBase.cs | 2 +- .../Formats/WebP/WebPDecoderCore.cs | 12 ++-- src/ImageSharp/Formats/WebP/WebPImageInfo.cs | 7 +- .../Formats/WebP/WebPLosslessDecoder.cs | 28 ++++---- .../Formats/WebP/WebPLossyDecoder.cs | 36 +++------- 8 files changed, 147 insertions(+), 107 deletions(-) create mode 100644 src/ImageSharp/Formats/WebP/BitReaderBase.cs diff --git a/src/ImageSharp/Formats/WebP/BitReaderBase.cs b/src/ImageSharp/Formats/WebP/BitReaderBase.cs new file mode 100644 index 0000000000..a07c68ed94 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/BitReaderBase.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +using SixLabors.ImageSharp.Memory; +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Base class for VP8 and VP8L bitreader. + /// + internal abstract class BitReaderBase + { + /// + /// Gets raw encoded image data. + /// + 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. + /// + /// The input stream. + /// Number of bytes to read as indicated from the chunk size. + /// Used for allocating memory during reading data from the stream. + protected void ReadImageDataFromStream(Stream input, int bytesToRead, MemoryAllocator memoryAllocator) + { + using (var ms = new MemoryStream()) + using (IManagedByteBuffer buffer = memoryAllocator.AllocateManagedByteBuffer(4096)) + { + Span bufferSpan = buffer.GetSpan(); + int read; + while (bytesToRead > 0 && (read = input.Read(buffer.Array, 0, Math.Min(bufferSpan.Length, bytesToRead))) > 0) + { + ms.Write(buffer.Array, 0, read); + bytesToRead -= read; + } + + if (bytesToRead > 0) + { + WebPThrowHelper.ThrowImageFormatException("image file has insufficient data"); + } + + this.Data = ms.ToArray(); + } + } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8BitReader.cs b/src/ImageSharp/Formats/WebP/Vp8BitReader.cs index bdb1d0e033..cac683b1f4 100644 --- a/src/ImageSharp/Formats/WebP/Vp8BitReader.cs +++ b/src/ImageSharp/Formats/WebP/Vp8BitReader.cs @@ -2,10 +2,16 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.IO; + +using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.WebP { - internal class Vp8BitReader + /// + /// A bit reader for VP8 streams. + /// + internal class Vp8BitReader : BitReaderBase { /// /// Current value. @@ -43,14 +49,34 @@ namespace SixLabors.ImageSharp.Formats.WebP private bool eof; /// - /// Reads the specified number of bits from read buffer. - /// Flags an error in case end_of_stream or n_bits is more than the allowed limit - /// of VP8L_MAX_NUM_BIT_READ (inclusive). - /// Flags eos_ if this read attempt is going to cross the read buffer. + /// Initializes a new instance of the class. /// - /// The number of bits to read. - public int ReadBits(int nBits) + /// The input stream to read from. + /// The raw image data size in bytes. + /// Used for allocating memory during reading data from the stream. + public Vp8BitReader(Stream inputStream, uint imageDataSize, MemoryAllocator memoryAllocator) + { + this.ReadImageDataFromStream(inputStream, (int)imageDataSize, memoryAllocator); + } + + /// + public override bool ReadBit() + { + throw new NotImplementedException(); + } + + /// + public override uint ReadValue(int nBits) { + Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + + throw new NotImplementedException(); + } + + public int ReadSignedValue(int nBits) + { + Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + throw new NotImplementedException(); } } diff --git a/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs b/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs index d3a13035dd..27dab46878 100644 --- a/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs +++ b/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs @@ -1,18 +1,16 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.WebP { /// - /// A bit reader for VP8 streams. + /// A bit reader for VP8L streams. /// - internal class Vp8LBitReader + internal class Vp8LBitReader : BitReaderBase { /// /// Maximum number of bits (inclusive) the bit-reader can handle. @@ -20,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.WebP private const int Vp8LMaxNumBitRead = 24; /// - /// Number of bits prefetched (= bit-size of vp8l_val_t). + /// Number of bits prefetched. /// private const int Vp8LLbits = 64; @@ -40,8 +38,6 @@ namespace SixLabors.ImageSharp.Formats.WebP 0x1fffff, 0x3fffff, 0x7fffff, 0xffffff }; - private readonly byte[] data; - /// /// Pre-fetched bits. /// @@ -71,17 +67,13 @@ namespace SixLabors.ImageSharp.Formats.WebP /// Initializes a new instance of the class. /// /// The input stream to read from. - /// The image data size in bytes. + /// The raw image data size in bytes. /// Used for allocating memory during reading data from the stream. public Vp8LBitReader(Stream inputStream, uint imageDataSize, MemoryAllocator memoryAllocator) { long length = imageDataSize; - using (var ms = new MemoryStream()) - { - CopyStream(inputStream, ms, (int)imageDataSize, memoryAllocator); - this.data = ms.ToArray(); - } + this.ReadImageDataFromStream(inputStream, (int)imageDataSize, memoryAllocator); this.len = length; this.value = 0; @@ -96,19 +88,15 @@ namespace SixLabors.ImageSharp.Formats.WebP ulong currentValue = 0; for (int i = 0; i < length; ++i) { - currentValue |= (ulong)this.data[i] << (8 * i); + currentValue |= (ulong)this.Data[i] << (8 * i); } this.value = currentValue; this.pos = length; } - /// - /// 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 uint ReadBits(int nBits) + /// + public override uint ReadValue(int nBits) { Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); @@ -125,13 +113,10 @@ namespace SixLabors.ImageSharp.Formats.WebP return 0; } - /// - /// Reads a single bit from the stream. - /// - /// True if the bit read was 1, false otherwise. - public bool ReadBit() + /// + public override bool ReadBit() { - uint bit = this.ReadBits(1); + uint bit = this.ReadValue(1); return bit != 0; } @@ -153,14 +138,14 @@ namespace SixLabors.ImageSharp.Formats.WebP } } - public void DoFillBitWindow() + public bool IsEndOfStream() { - this.ShiftBytes(); + return this.eos || ((this.pos == this.len) && (this.bitPos > Vp8LLbits)); } - public bool IsEndOfStream() + private void DoFillBitWindow() { - return this.eos || ((this.pos == this.len) && (this.bitPos > Vp8LLbits)); + this.ShiftBytes(); } /// @@ -171,7 +156,7 @@ namespace SixLabors.ImageSharp.Formats.WebP while (this.bitPos >= 8 && this.pos < this.len) { this.value >>= 8; - this.value |= (ulong)this.data[this.pos] << (Vp8LLbits - 8); + this.value |= (ulong)this.Data[this.pos] << (Vp8LLbits - 8); ++this.pos; this.bitPos -= 8; } @@ -187,24 +172,5 @@ namespace SixLabors.ImageSharp.Formats.WebP this.eos = true; this.bitPos = 0; // To avoid undefined behaviour with shifts. } - - private static void CopyStream(Stream input, Stream output, int bytesToRead, MemoryAllocator memoryAllocator) - { - using (IManagedByteBuffer buffer = memoryAllocator.AllocateManagedByteBuffer(4096)) - { - Span bufferSpan = buffer.GetSpan(); - int read; - while (bytesToRead > 0 && (read = input.Read(buffer.Array, 0, Math.Min(bufferSpan.Length, bytesToRead))) > 0) - { - output.Write(buffer.Array, 0, read); - bytesToRead -= read; - } - - if (bytesToRead > 0) - { - WebPThrowHelper.ThrowImageFormatException("image file has insufficient data"); - } - } - } } } diff --git a/src/ImageSharp/Formats/WebP/WebPDecoderBase.cs b/src/ImageSharp/Formats/WebP/WebPDecoderBase.cs index ecbf68a39b..c2cc2c0681 100644 --- a/src/ImageSharp/Formats/WebP/WebPDecoderBase.cs +++ b/src/ImageSharp/Formats/WebP/WebPDecoderBase.cs @@ -38,7 +38,7 @@ namespace SixLabors.ImageSharp.Formats.WebP int extraBits = (distanceSymbol - 2) >> 1; int offset = (2 + (distanceSymbol & 1)) << extraBits; - return (int)(offset + bitReader.ReadBits(extraBits) + 1); + return (int)(offset + bitReader.ReadValue(extraBits) + 1); } // TODO: copied from WebPLosslessDecoder diff --git a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs index 214b385e41..415c0a88d3 100644 --- a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Formats.WebP else { var lossyDecoder = new WebPLossyDecoder(this.configuration, this.currentStream); - lossyDecoder.Decode(pixels, image.Width, image.Height, (int)imageInfo.ImageDataSize); + lossyDecoder.Decode(pixels, image.Width, image.Height, imageInfo.ImageDataSize, imageInfo.Vp8Profile); } // There can be optional chunks after the image data, like EXIF and XMP. @@ -261,7 +261,7 @@ namespace SixLabors.ImageSharp.Formats.WebP // A VP8 or VP8L chunk should follow here. chunkType = this.ReadChunkType(); - // TOOD: image width and height from VP8X should overrule VP8 or VP8L info, because its 3 bytes instead of just 14 bit. + // TOOD: check if VP8 or VP8L info about the dimensions match VP8X info switch (chunkType) { case WebPChunkType.Vp8: @@ -361,15 +361,15 @@ namespace SixLabors.ImageSharp.Formats.WebP var bitReader = new Vp8LBitReader(this.currentStream, imageDataSize, this.memoryAllocator); // One byte signature, should be 0x2f. - uint signature = bitReader.ReadBits(8); + uint signature = bitReader.ReadValue(8); if (signature != WebPConstants.Vp8LMagicByte) { WebPThrowHelper.ThrowImageFormatException("Invalid VP8L signature"); } // The first 28 bits of the bitstream specify the width and height of the image. - uint width = bitReader.ReadBits(WebPConstants.Vp8LImageSizeBits) + 1; - uint height = bitReader.ReadBits(WebPConstants.Vp8LImageSizeBits) + 1; + uint width = bitReader.ReadValue(WebPConstants.Vp8LImageSizeBits) + 1; + uint height = bitReader.ReadValue(WebPConstants.Vp8LImageSizeBits) + 1; if (width is 0 || height is 0) { WebPThrowHelper.ThrowImageFormatException("width or height can not be zero"); @@ -381,7 +381,7 @@ namespace SixLabors.ImageSharp.Formats.WebP // The next 3 bits are the version. The version_number is a 3 bit code that must be set to 0. // Any other value should be treated as an error. // TODO: should we throw here when version number is != 0? - uint version = bitReader.ReadBits(WebPConstants.Vp8LVersionBits); + uint version = bitReader.ReadValue(WebPConstants.Vp8LVersionBits); return new WebPImageInfo() { diff --git a/src/ImageSharp/Formats/WebP/WebPImageInfo.cs b/src/ImageSharp/Formats/WebP/WebPImageInfo.cs index 14d2c0ff74..c9fd9bd513 100644 --- a/src/ImageSharp/Formats/WebP/WebPImageInfo.cs +++ b/src/ImageSharp/Formats/WebP/WebPImageInfo.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.WebP public WebPBitsPerPixel BitsPerPixel { get; set; } /// - /// Gets or sets a value indicating whether this image uses a lossless compression. + /// Gets or sets a value indicating whether this image uses lossless compression. /// public bool IsLossLess { get; set; } @@ -35,6 +35,11 @@ namespace SixLabors.ImageSharp.Formats.WebP /// public uint ImageDataSize { get; set; } + /// + /// Gets or sets the VP8 profile / version. Valid values are between 0 and 3. + /// + public byte Vp8Profile { get; set; } + /// /// Gets or sets Vp8L bitreader. Will be null if its not lossless image. /// diff --git a/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs b/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs index 851ac213b4..40f13aaea3 100644 --- a/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs +++ b/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs @@ -125,7 +125,7 @@ namespace SixLabors.ImageSharp.Formats.WebP int colorCacheSize = 0; if (colorCachePresent) { - colorCacheBits = (int)this.bitReader.ReadBits(4); + colorCacheBits = (int)this.bitReader.ReadValue(4); bool coloCacheBitsIsValid = colorCacheBits >= 1 && colorCacheBits <= WebPConstants.MaxColorCacheBits; if (!coloCacheBitsIsValid) { @@ -355,7 +355,7 @@ namespace SixLabors.ImageSharp.Formats.WebP if (allowRecursion && this.bitReader.ReadBit()) { // Use meta Huffman codes. - uint huffmanPrecision = this.bitReader.ReadBits(3) + 2; + uint huffmanPrecision = this.bitReader.ReadValue(3) + 2; int huffmanXSize = LosslessUtils.SubSampleSize(xSize, (int)huffmanPrecision); int huffmanYSize = LosslessUtils.SubSampleSize(ySize, (int)huffmanPrecision); int huffmanPixels = huffmanXSize * huffmanYSize; @@ -487,17 +487,17 @@ namespace SixLabors.ImageSharp.Formats.WebP // and are in the range of[0, 255].All other Huffman code lengths are implicitly zeros. // Read symbols, codes & code lengths directly. - uint numSymbols = this.bitReader.ReadBits(1) + 1; - uint firstSymbolLenCode = this.bitReader.ReadBits(1); + uint numSymbols = this.bitReader.ReadValue(1) + 1; + uint firstSymbolLenCode = this.bitReader.ReadValue(1); // The first code is either 1 bit or 8 bit code. - uint symbol = this.bitReader.ReadBits((firstSymbolLenCode is 0) ? 1 : 8); + uint symbol = this.bitReader.ReadValue((firstSymbolLenCode is 0) ? 1 : 8); codeLengths[symbol] = 1; // The second code (if present), is always 8 bit long. if (numSymbols is 2) { - symbol = this.bitReader.ReadBits(8); + symbol = this.bitReader.ReadValue(8); codeLengths[symbol] = 1; } } @@ -507,7 +507,7 @@ namespace SixLabors.ImageSharp.Formats.WebP // The code lengths of a Huffman code are read as follows: num_code_lengths specifies the number of code lengths; // the rest of the code lengths (according to the order in kCodeLengthCodeOrder) are zeros. var codeLengthCodeLengths = new int[NumCodeLengthCodes]; - uint numCodes = this.bitReader.ReadBits(4) + 4; + uint numCodes = this.bitReader.ReadValue(4) + 4; if (numCodes > NumCodeLengthCodes) { WebPThrowHelper.ThrowImageFormatException("Bitstream error, numCodes has an invalid value"); @@ -515,7 +515,7 @@ namespace SixLabors.ImageSharp.Formats.WebP for (int i = 0; i < numCodes; i++) { - codeLengthCodeLengths[KCodeLengthCodeOrder[i]] = (int)this.bitReader.ReadBits(3); + codeLengthCodeLengths[KCodeLengthCodeOrder[i]] = (int)this.bitReader.ReadValue(3); } this.ReadHuffmanCodeLengths(table.ToArray(), codeLengthCodeLengths, alphabetSize, codeLengths); @@ -539,8 +539,8 @@ namespace SixLabors.ImageSharp.Formats.WebP if (this.bitReader.ReadBit()) { - int lengthNBits = 2 + (2 * (int)this.bitReader.ReadBits(3)); - maxSymbol = 2 + (int)this.bitReader.ReadBits(lengthNBits); + int lengthNBits = 2 + (2 * (int)this.bitReader.ReadValue(3)); + maxSymbol = 2 + (int)this.bitReader.ReadValue(lengthNBits); } else { @@ -574,7 +574,7 @@ namespace SixLabors.ImageSharp.Formats.WebP uint slot = codeLen - WebPConstants.KCodeLengthLiterals; int extraBits = WebPConstants.KCodeLengthExtraBits[slot]; int repeatOffset = WebPConstants.KCodeLengthRepeatOffsets[slot]; - int repeat = (int)(this.bitReader.ReadBits(extraBits) + repeatOffset); + int repeat = (int)(this.bitReader.ReadValue(extraBits) + repeatOffset); if (symbol + repeat > numSymbols) { // TODO: not sure, if this should be treated as an error here @@ -598,7 +598,7 @@ namespace SixLabors.ImageSharp.Formats.WebP /// Vp8LDecoder where the transformations will be stored. private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) { - var transformType = (Vp8LTransformType)this.bitReader.ReadBits(2); + var transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); var transform = new Vp8LTransform(transformType, xSize, ySize); // Each transform is allowed to be used only once. @@ -615,7 +615,7 @@ namespace SixLabors.ImageSharp.Formats.WebP case Vp8LTransformType.ColorIndexingTransform: // The transform data contains color table size and the entries in the color table. // 8 bit value for color table size. - uint numColors = this.bitReader.ReadBits(8) + 1; + uint numColors = this.bitReader.ReadValue(8) + 1; int bits = (numColors > 16) ? 0 : (numColors > 4) ? 1 : (numColors > 2) ? 2 @@ -636,7 +636,7 @@ namespace SixLabors.ImageSharp.Formats.WebP case Vp8LTransformType.CrossColorTransform: { // The first 3 bits of prediction data define the block width and height in number of bits. - transform.Bits = (int)this.bitReader.ReadBits(3) + 2; + transform.Bits = (int)this.bitReader.ReadValue(3) + 2; int blockWidth = LosslessUtils.SubSampleSize(transform.XSize, transform.Bits); int blockHeight = LosslessUtils.SubSampleSize(transform.YSize, transform.Bits); IMemoryOwner transformData = this.DecodeImageStream(decoder, blockWidth, blockHeight, false); diff --git a/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs b/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs index c1293ff498..896c3e256c 100644 --- a/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs +++ b/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs @@ -18,48 +18,26 @@ namespace SixLabors.ImageSharp.Formats.WebP private MemoryAllocator memoryAllocator; - public WebPLossyDecoder( - Configuration configuration, - Stream currentStream) + public WebPLossyDecoder(Configuration configuration, Stream currentStream) { this.configuration = configuration; this.currentStream = currentStream; this.memoryAllocator = configuration.MemoryAllocator; } - public void Decode(Buffer2D pixels, int width, int height, int imageDataSize) + public void Decode(Buffer2D pixels, int width, int height, uint imageDataSize, byte vp8Version) where TPixel : struct, IPixel { - // TODO: implement decoding. For simulating the decoding: skipping the chunk size bytes. - this.currentStream.Skip(imageDataSize - 10); // TODO: Not sure why we need to skip 10 bytes less here - // we need buffers for Y U and V in size of the image // TODO: increase size to enable using all prediction blocks? (see https://tools.ietf.org/html/rfc6386#page-9 ) - Buffer2D yuvBufferCurrentFrame = - this.memoryAllocator - .Allocate2D(width, height); + Buffer2D yuvBufferCurrentFrame = this.memoryAllocator.Allocate2D(width, height); // TODO: var predictionBuffer - macro-block-sized with approximation of the portion of the image being reconstructed. // those prediction values are the base, the values from DCT processing are added to that // TODO residue signal from DCT: 4x4 blocks of DCT transforms, 16Y, 4U, 4V - // TODO weiter bei S.11 - - // bit STREAM: See https://tools.ietf.org/html/rfc6386#page-29 ("Frame Header") - // TODO: Vp8BitReader should be used here instead - /*Vp8LBitReader bitReader = new Vp8LBitReader(this.currentStream); - bool isInterframe = bitReader.ReadBit(); - if (isInterframe) - { - throw new NotImplementedException("only key frames supported yet"); - } - - byte version = (byte)((bitReader.ReadBit() ? 2 : 0) | (bitReader.ReadBit() ? 1 : 0)); - (ReconstructionFilter rec, LoopFilter loop) = this.DecodeVersion(version); - - bool isShowFrame = bitReader.ReadBit(); - - uint firstPartitionSize = (bitReader.ReadBits(16) << 3) | bitReader.ReadBits(3);*/ + var bitReader = new Vp8BitReader(this.currentStream, imageDataSize, this.memoryAllocator); + (ReconstructionFilter rec, LoopFilter loop) = this.DecodeVersion(vp8Version); } private (ReconstructionFilter, LoopFilter) DecodeVersion(byte version) @@ -78,8 +56,10 @@ namespace SixLabors.ImageSharp.Formats.WebP case 3: return (ReconstructionFilter.None, LoopFilter.None); default: + // Reserved for future use in Spec. // https://tools.ietf.org/html/rfc6386#page-30 - throw new NotSupportedException("reserved for future use in Spec"); + WebPThrowHelper.ThrowNotSupportedException($"unsupported VP8 version {version} found"); + return (rec, loop); } } }