📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

581 lines
23 KiB

// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp
{
/// <summary>
/// Performs the webp decoding operation.
/// </summary>
internal sealed class WebpDecoderCore : IImageDecoderInternals
{
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[4];
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The stream to decode from.
/// </summary>
private Stream currentStream;
/// <summary>
/// The webp specific metadata.
/// </summary>
private WebpMetadata webpMetadata;
/// <summary>
/// Information about the webp image.
/// </summary>
private WebpImageInfo webImageInfo;
/// <summary>
/// Initializes a new instance of the <see cref="WebpDecoderCore"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="options">The options.</param>
public WebpDecoderCore(Configuration configuration, IWebpDecoderOptions options)
{
this.Configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.IgnoreMetadata = options.IgnoreMetadata;
}
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
public bool IgnoreMetadata { get; }
/// <summary>
/// Gets the <see cref="ImageMetadata"/> decoded by this decoder instance.
/// </summary>
public ImageMetadata Metadata { get; private set; }
/// <inheritdoc/>
public Configuration Configuration { get; }
/// <summary>
/// Gets the dimensions of the image.
/// </summary>
public Size Dimensions => new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height);
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
this.Metadata = new ImageMetadata();
this.currentStream = stream;
uint fileSize = this.ReadImageHeader();
using (this.webImageInfo = this.ReadVp8Info())
{
if (this.webImageInfo.Features is { Animation: true })
{
WebpThrowHelper.ThrowNotSupportedException("Animations are not supported");
}
var image = new Image<TPixel>(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless)
{
var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration);
losslessDecoder.Decode(pixels, image.Width, image.Height);
}
else
{
var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration);
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo);
}
// There can be optional chunks after the image data, like EXIF and XMP.
if (this.webImageInfo.Features != null)
{
this.ParseOptionalChunks(this.webImageInfo.Features);
}
return image;
}
}
/// <inheritdoc />
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.currentStream = stream;
this.ReadImageHeader();
using (this.webImageInfo = this.ReadVp8Info())
{
return new ImageInfo(new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata);
}
}
/// <summary>
/// Reads and skips over the image header.
/// </summary>
/// <returns>The file size in bytes.</returns>
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 fileSize = this.ReadChunkSize();
// Skip 'WEBP' from the header.
this.currentStream.Skip(4);
return fileSize;
}
/// <summary>
/// Reads information present in the image header, about the image content and how to decode the image.
/// </summary>
/// <returns>Information about the webp image.</returns>
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();
default:
WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header");
return new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception.
}
}
/// <summary>
/// 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 'XMP' chunk with metadata.
/// - 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.
/// </summary>
/// <returns>Information about this webp image.</returns>
private WebpImageInfo ReadVp8XHeader()
{
var features = new WebpFeatures();
uint fileSize = this.ReadChunkSize();
// The first byte contains information about the image features used.
byte imageFeatures = (byte)this.currentStream.ReadByte();
// The first two bit of it are reserved and should be 0.
if (imageFeatures >> 6 != 0)
{
WebpThrowHelper.ThrowImageFormatException("first two bits of the VP8X header are expected to be zero");
}
// If bit 3 is set, a ICC Profile Chunk should be present.
features.IccProfile = (imageFeatures & (1 << 5)) != 0;
// If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk).
features.Alpha = (imageFeatures & (1 << 4)) != 0;
// If bit 5 is set, a EXIF metadata should be present.
features.ExifProfile = (imageFeatures & (1 << 3)) != 0;
// If bit 6 is set, XMP metadata should be present.
features.XmpMetaData = (imageFeatures & (1 << 2)) != 0;
// If bit 7 is set, animation should be present.
features.Animation = (imageFeatures & (1 << 1)) != 0;
// 3 reserved bytes should follow which are supposed to be zero.
this.currentStream.Read(this.buffer, 0, 3);
if (this.buffer[0] != 0 || this.buffer[1] != 0 || this.buffer[2] != 0)
{
WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero");
}
// 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;
// Read all the chunks in the order they occur.
var info = new WebpImageInfo();
while (this.currentStream.Position < this.currentStream.Length)
{
WebpChunkType chunkType = this.ReadChunkType();
if (chunkType == WebpChunkType.Vp8)
{
info = this.ReadVp8Header(features);
}
else if (chunkType == WebpChunkType.Vp8L)
{
info = this.ReadVp8LHeader(features);
}
else if (IsOptionalVp8XChunk(chunkType))
{
this.ParseOptionalExtendedChunks(chunkType, features);
}
else
{
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
}
}
if (features.Animation)
{
// TODO: Animations are not yet supported.
return new WebpImageInfo() { Width = width, Height = height, Features = features };
}
return info;
}
/// <summary>
/// Reads the header of a lossy webp image.
/// </summary>
/// <param name="features">Webp features.</param>
/// <returns>Information about this webp image.</returns>
private WebpImageInfo ReadVp8Header(WebpFeatures features = null)
{
this.webpMetadata.FileFormat = WebpFileFormatType.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 isNoKeyFrame = (frameTag & 0x1) == 1;
if (isNoKeyFrame)
{
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 invisibleFrame = ((frameTag >> 4) & 0x1) == 0;
if (invisibleFrame)
{
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(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
{
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 == 0 || height == 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)
{
Remaining = remaining
};
return new WebpImageInfo()
{
Width = width,
Height = height,
XScale = xScale,
YScale = yScale,
BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Pixel32 : WebpBitsPerPixel.Pixel24,
IsLossless = false,
Features = features,
Vp8Profile = (sbyte)version,
Vp8FrameHeader = vp8FrameHeader,
Vp8BitReader = bitReader
};
}
/// <summary>
/// Reads the header of a lossless webp image.
/// </summary>
/// <param name="features">Webp image features.</param>
/// <returns>Information about this image.</returns>
private WebpImageInfo ReadVp8LHeader(WebpFeatures features = null)
{
this.webpMetadata.FileFormat = WebpFileFormatType.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.Vp8LHeaderMagicByte)
{
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 == 0 || height == 0)
{
WebpThrowHelper.ThrowImageFormatException("invalid width or height read");
}
// 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
};
}
/// <summary>
/// Parses optional VP8X chunks, which can be ICCP, XMP, ANIM or ALPH chunks.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
/// <param name="features">The webp image features.</param>
private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features)
{
switch (chunkType)
{
case WebpChunkType.Iccp:
uint iccpChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)iccpChunkSize);
}
else
{
byte[] iccpData = new byte[iccpChunkSize];
this.currentStream.Read(iccpData, 0, (int)iccpChunkSize);
var profile = new IccProfile(iccpData);
if (profile.CheckIsValid())
{
this.Metadata.IccProfile = profile;
}
}
break;
case WebpChunkType.Exif:
uint exifChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)exifChunkSize);
}
else
{
byte[] exifData = new byte[exifChunkSize];
this.currentStream.Read(exifData, 0, (int)exifChunkSize);
var profile = new ExifProfile(exifData);
this.Metadata.ExifProfile = profile;
}
break;
case WebpChunkType.Xmp:
uint xmpChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)xmpChunkSize);
}
else
{
byte[] xmpData = new byte[xmpChunkSize];
this.currentStream.Read(xmpData, 0, (int)xmpChunkSize);
var profile = new XmpProfile(xmpData);
this.Metadata.XmpProfile = profile;
}
break;
case WebpChunkType.Animation:
// TODO: Decoding animation is not implemented yet.
break;
case WebpChunkType.Alpha:
uint alphaChunkSize = this.ReadChunkSize();
features.AlphaChunkHeader = (byte)this.currentStream.ReadByte();
int alphaDataSize = (int)(alphaChunkSize - 1);
features.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize);
this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize);
break;
default:
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
break;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="features">The webp features.</param>
private void ParseOptionalChunks(WebpFeatures features)
{
if (this.IgnoreMetadata || (features.ExifProfile == false && features.XmpMetaData == false))
{
return;
}
long streamLength = this.currentStream.Length;
while (this.currentStream.Position < streamLength)
{
// Read chunk header.
WebpChunkType chunkType = this.ReadChunkType();
uint chunkLength = this.ReadChunkSize();
if (chunkType == WebpChunkType.Exif && this.Metadata.ExifProfile == null)
{
byte[] exifData = new byte[chunkLength];
this.currentStream.Read(exifData, 0, (int)chunkLength);
this.Metadata.ExifProfile = new ExifProfile(exifData);
}
else
{
// Skip XMP chunk data or any duplicate EXIF chunk.
this.currentStream.Skip((int)chunkLength);
}
}
}
/// <summary>
/// Identifies the chunk type from the chunk.
/// </summary>
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid.
/// </exception>
private WebpChunkType ReadChunkType()
{
if (this.currentStream.Read(this.buffer, 0, 4) == 4)
{
var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer);
return chunkType;
}
throw new ImageFormatException("Invalid Webp data.");
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The chunk size in bytes.</returns>
private uint ReadChunkSize()
{
if (this.currentStream.Read(this.buffer, 0, 4) == 4)
{
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer);
return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
}
throw new ImageFormatException("Invalid Webp data.");
}
/// <summary>
/// Determines if the chunk type is an optional VP8X chunk.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
/// <returns>True, if its an optional chunk type.</returns>
private static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch
{
WebpChunkType.Alpha => true,
WebpChunkType.Animation => true,
WebpChunkType.Exif => true,
WebpChunkType.Iccp => true,
WebpChunkType.Xmp => true,
_ => false
};
}
}