// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; namespace SixLabors.ImageSharp.Formats.WebP { /// /// Performs the bitmap decoding operation. /// internal sealed class WebPDecoderCore { /// /// Reusable buffer. /// private readonly byte[] buffer = new byte[4]; /// /// The global configuration. /// private readonly Configuration configuration; /// /// Used for allocating memory during processing operations. /// private readonly MemoryAllocator memoryAllocator; /// /// The stream to decode from. /// private Stream currentStream; /// /// The webp specific metadata. /// private WebPMetadata webpMetadata; /// /// Initializes a new instance of the class. /// /// The configuration. /// The options. public WebPDecoderCore(Configuration configuration, IWebPDecoderOptions options) { this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; this.IgnoreMetadata = options.IgnoreMetadata; } /// /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. /// public bool IgnoreMetadata { get; } /// /// Gets the decoded by this decoder instance. /// public ImageMetadata Metadata { get; private set; } /// /// Decodes the image from the specified and sets the data to the image. /// /// The pixel format. /// The stream, where the image should be. /// The decoded image. public Image Decode(Stream stream) where TPixel : struct, IPixel { this.Metadata = new ImageMetadata(); this.currentStream = stream; uint fileSize = this.ReadImageHeader(); WebPImageInfo imageInfo = this.ReadVp8Info(); if (imageInfo.Features != null && imageInfo.Features.Animation) { WebPThrowHelper.ThrowNotSupportedException("Animations are not supported"); } var image = new Image(this.configuration, (int)imageInfo.Width, (int)imageInfo.Height, this.Metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); if (imageInfo.IsLossLess) { var losslessDecoder = new WebPLosslessDecoder(imageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixels, image.Width, image.Height); } else { var lossyDecoder = new WebPLossyDecoder(imageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixels, image.Width, image.Height, imageInfo); } // There can be optional chunks after the image data, like EXIF and XMP. if (imageInfo.Features != null) { this.ParseOptionalChunks(imageInfo.Features); } return image; } /// /// Reads the raw image information from the specified stream. /// /// The containing image data. public IImageInfo Identify(Stream stream) { this.currentStream = stream; this.ReadImageHeader(); WebPImageInfo imageInfo = this.ReadVp8Info(); return new ImageInfo(new PixelTypeInfo((int)imageInfo.BitsPerPixel), (int)imageInfo.Width, (int)imageInfo.Height, this.Metadata); } /// /// Reads and skips over the image header. /// /// The chunk size in bytes. private uint ReadImageHeader() { // Skip FourCC header, we already know its a RIFF file at this point. this.currentStream.Skip(4); // Read file size. // The size of the file in bytes starting at offset 8. // The file size in the header is the total size of the chunks that follow plus 4 bytes for the ‘WEBP’ FourCC. uint chunkSize = this.ReadChunkSize(); // Skip 'WEBP' from the header. this.currentStream.Skip(4); return chunkSize; } private WebPImageInfo ReadVp8Info() { this.Metadata = new ImageMetadata(); this.webpMetadata = this.Metadata.GetFormatMetadata(WebPFormat.Instance); WebPChunkType chunkType = this.ReadChunkType(); switch (chunkType) { case WebPChunkType.Vp8: return this.ReadVp8Header(); case WebPChunkType.Vp8L: return this.ReadVp8LHeader(); case WebPChunkType.Vp8X: return this.ReadVp8XHeader(); } WebPThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); return new WebPImageInfo(); } /// /// Reads an the extended webp file header. An extended file header consists of: /// - A 'VP8X' chunk with information about features used in the file. /// - An optional 'ICCP' chunk with color profile. /// - An optional 'ANIM' chunk with animation control data. /// - An optional 'ALPH' chunk with alpha channel data. /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow. /// /// Information about this webp image. private WebPImageInfo ReadVp8XHeader() { uint chunkSize = this.ReadChunkSize(); // The first byte contains information about the image features used. // The first two bit of it are reserved and should be 0. TODO: should an exception be thrown if its not the case, or just ignore it? byte imageFeatures = (byte)this.currentStream.ReadByte(); // If bit 3 is set, a ICC Profile Chunk should be present. bool isIccPresent = (imageFeatures & (1 << 5)) != 0; // If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk). bool isAlphaPresent = (imageFeatures & (1 << 4)) != 0; // If bit 5 is set, a EXIF metadata should be present. bool isExifPresent = (imageFeatures & (1 << 3)) != 0; // If bit 6 is set, XMP metadata should be present. bool isXmpPresent = (imageFeatures & (1 << 2)) != 0; // If bit 7 is set, animation should be present. bool isAnimationPresent = (imageFeatures & (1 << 1)) != 0; // 3 reserved bytes should follow which are supposed to be zero. this.currentStream.Read(this.buffer, 0, 3); // 3 bytes for the width. this.currentStream.Read(this.buffer, 0, 3); this.buffer[3] = 0; uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; // 3 bytes for the height. this.currentStream.Read(this.buffer, 0, 3); this.buffer[3] = 0; uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; // Optional chunks ICCP, ALPH and ANIM can follow here. WebPChunkType chunkType; if (isIccPresent) { chunkType = this.ReadChunkType(); if (chunkType is WebPChunkType.Iccp) { uint iccpChunkSize = this.ReadChunkSize(); var iccpData = new byte[iccpChunkSize]; this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); var profile = new IccProfile(iccpData); if (profile.CheckIsValid()) { this.Metadata.IccProfile = profile; } } } if (isAnimationPresent) { this.webpMetadata.Animated = true; return new WebPImageInfo() { Width = width, Height = height, Features = new WebPFeatures() { Animation = true } }; } byte[] alphaData = null; byte alphaChunkHeader = 0; if (isAlphaPresent) { chunkType = this.ReadChunkType(); if (chunkType != WebPChunkType.Alpha) { WebPThrowHelper.ThrowImageFormatException($"unexpected chunk type {chunkType}, expected ALPH chunk is missing"); } uint alphaChunkSize = this.ReadChunkSize(); alphaChunkHeader = (byte)this.currentStream.ReadByte(); alphaData = new byte[alphaChunkSize - 1]; this.currentStream.Read(alphaData, 0, alphaData.Length); } var features = new WebPFeatures() { Animation = isAnimationPresent, Alpha = isAlphaPresent, AlphaData = alphaData, AlphaChunkHeader = alphaChunkHeader, ExifProfile = isExifPresent, IccProfile = isIccPresent, XmpMetaData = isXmpPresent }; // A VP8 or VP8L chunk should follow here. chunkType = this.ReadChunkType(); // TOOD: check if VP8 or VP8L info about the dimensions match VP8X info switch (chunkType) { case WebPChunkType.Vp8: return this.ReadVp8Header(features); case WebPChunkType.Vp8L: return this.ReadVp8LHeader(features); } WebPThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); return new WebPImageInfo(); } /// /// Reads the header of a lossy webp image. /// /// Webp features. /// Information about this webp image. private WebPImageInfo ReadVp8Header(WebPFeatures features = null) { this.webpMetadata.Format = WebPFormatType.Lossy; // VP8 data size (not including this 4 bytes). this.currentStream.Read(this.buffer, 0, 4); uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); // remaining counts the available image data payload. uint remaining = dataSize; // 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. // - A 1-bit show_frame flag. // - A 19-bit field containing the size of the first data partition in bytes. this.currentStream.Read(this.buffer, 0, 3); uint frameTag = (uint)(this.buffer[0] | (this.buffer[1] << 8) | (this.buffer[2] << 16)); remaining -= 3; bool isKeyFrame = (frameTag & 0x1) is 0; if (!isKeyFrame) { WebPThrowHelper.ThrowImageFormatException("VP8 header indicates the image is not a key frame"); } uint version = (frameTag >> 1) & 0x7; if (version > 3) { WebPThrowHelper.ThrowImageFormatException($"VP8 header indicates unknown profile {version}"); } bool showFrame = ((frameTag >> 4) & 0x1) is 1; if (!showFrame) { WebPThrowHelper.ThrowImageFormatException("VP8 header indicates that the first frame is invisible"); } uint partitionLength = frameTag >> 5; if (partitionLength > dataSize) { WebPThrowHelper.ThrowImageFormatException("VP8 header contains inconsistent size information"); } // Check for VP8 magic bytes. this.currentStream.Read(this.buffer, 0, 3); if (!this.buffer.AsSpan().Slice(0, 3).SequenceEqual(WebPConstants.Vp8MagicBytes)) { WebPThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); } this.currentStream.Read(this.buffer, 0, 4); 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); remaining -= 7; if (width is 0 || height is 0) { WebPThrowHelper.ThrowImageFormatException("width or height can not be zero"); } if (partitionLength > remaining) { WebPThrowHelper.ThrowImageFormatException("bad partition length"); } var vp8FrameHeader = new Vp8FrameHeader() { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; var bitReader = new Vp8BitReader( this.currentStream, remaining, this.memoryAllocator, partitionLength); bitReader.Remaining = remaining; return new WebPImageInfo() { Width = width, Height = height, XScale = xScale, YScale = yScale, BitsPerPixel = features?.Alpha is true ? WebPBitsPerPixel.Pixel32 : WebPBitsPerPixel.Pixel24, IsLossLess = false, Features = features, Vp8Profile = (sbyte)version, Vp8FrameHeader = vp8FrameHeader, Vp8BitReader = bitReader }; } /// /// Reads the header of a lossless webp image. /// /// Webp image features. /// Information about this image. private WebPImageInfo ReadVp8LHeader(WebPFeatures features = null) { this.webpMetadata.Format = WebPFormatType.Lossless; // VP8 data size. uint imageDataSize = this.ReadChunkSize(); var bitReader = new Vp8LBitReader(this.currentStream, imageDataSize, this.memoryAllocator); // One byte signature, should be 0x2f. 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.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"); } // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise. // TODO: this flag value is not used yet bool alphaIsUsed = bitReader.ReadBit(); // 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. uint version = bitReader.ReadValue(WebPConstants.Vp8LVersionBits); if (version != 0) { WebPThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } return new WebPImageInfo() { Width = width, Height = height, BitsPerPixel = WebPBitsPerPixel.Pixel32, IsLossLess = true, Features = features, Vp8LBitReader = bitReader }; } /// /// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP '). /// If there are more such chunks, readers MAY ignore all except the first one. /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks. /// /// The webp features. private void ParseOptionalChunks(WebPFeatures features) { if (this.IgnoreMetadata || (features.ExifProfile is false && features.XmpMetaData is false)) { return; } while (this.currentStream.Position < this.currentStream.Length) { // Read chunk header. WebPChunkType chunkType = this.ReadChunkType(); uint chunkLength = this.ReadChunkSize(); if (chunkType is WebPChunkType.Exif) { var exifData = new byte[chunkLength]; this.currentStream.Read(exifData, 0, (int)chunkLength); this.Metadata.ExifProfile = new ExifProfile(exifData); } else { // Skip XMP chunk data for now. this.currentStream.Skip((int)chunkLength); } } } /// /// Identifies the chunk type from the chunk. /// /// /// Thrown if the input stream is not valid. /// private WebPChunkType ReadChunkType() { if (this.currentStream.Read(this.buffer, 0, 4) is 4) { var chunkType = (WebPChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer); this.webpMetadata.ChunkTypes.Enqueue(chunkType); return chunkType; } throw new ImageFormatException("Invalid WebP data."); } /// /// Reads the chunk size. If Chunk Size is odd, a single padding byte will be added to the payload, /// so the chunk size will be increased by 1 in those cases. /// /// The chunk size in bytes. private uint ReadChunkSize() { if (this.currentStream.Read(this.buffer, 0, 4) is 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); return (chunkSize % 2 is 0) ? chunkSize : chunkSize + 1; } throw new ImageFormatException("Invalid WebP data."); } } }