mirror of https://github.com/SixLabors/ImageSharp
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.
381 lines
14 KiB
381 lines
14 KiB
// 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.PixelFormats;
|
|
using SixLabors.Memory;
|
|
|
|
namespace SixLabors.ImageSharp.Formats.WebP
|
|
{
|
|
/// <summary>
|
|
/// Performs the bitmap decoding operation.
|
|
/// </summary>
|
|
internal sealed class WebPDecoderCore
|
|
{
|
|
/// <summary>
|
|
/// Reusable buffer.
|
|
/// </summary>
|
|
private readonly byte[] buffer = new byte[4];
|
|
|
|
/// <summary>
|
|
/// The global configuration.
|
|
/// </summary>
|
|
private readonly Configuration configuration;
|
|
|
|
/// <summary>
|
|
/// Used for allocating memory during processing operations.
|
|
/// </summary>
|
|
private readonly MemoryAllocator memoryAllocator;
|
|
|
|
/// <summary>
|
|
/// The bitmap decoder options.
|
|
/// </summary>
|
|
private readonly IWebPDecoderOptions options;
|
|
|
|
/// <summary>
|
|
/// The stream to decode from.
|
|
/// </summary>
|
|
private Stream currentStream;
|
|
|
|
/// <summary>
|
|
/// The metadata.
|
|
/// </summary>
|
|
private ImageMetadata metadata;
|
|
|
|
/// <summary>
|
|
/// The webp specific metadata.
|
|
/// </summary>
|
|
private WebPMetadata webpMetadata;
|
|
|
|
/// <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.options = options;
|
|
}
|
|
|
|
public Image<TPixel> Decode<TPixel>(Stream stream)
|
|
where TPixel : struct, IPixel<TPixel>
|
|
{
|
|
this.currentStream = stream;
|
|
|
|
uint fileSize = this.ReadImageHeader();
|
|
WebPImageInfo imageInfo = this.ReadVp8Info();
|
|
if (imageInfo.IsAnimation)
|
|
{
|
|
WebPThrowHelper.ThrowNotSupportedException("Animations are not supported");
|
|
}
|
|
|
|
var image = new Image<TPixel>(this.configuration, imageInfo.Width, imageInfo.Height, this.metadata);
|
|
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
|
|
if (imageInfo.IsLossLess)
|
|
{
|
|
ReadSimpleLossless(pixels, image.Width, image.Height, (int)imageInfo.ImageDataSize);
|
|
}
|
|
else
|
|
{
|
|
ReadSimpleLossy(pixels, image.Width, image.Height, (int)imageInfo.ImageDataSize);
|
|
}
|
|
|
|
// There can be optional chunks after the image data, like EXIF, XMP etc.
|
|
this.ParseOptionalChunks();
|
|
|
|
return image;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the raw image information from the specified stream.
|
|
/// </summary>
|
|
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
|
|
public IImageInfo Identify(Stream stream)
|
|
{
|
|
this.currentStream = stream;
|
|
|
|
this.ReadImageHeader();
|
|
WebPImageInfo imageInfo = this.ReadVp8Info();
|
|
|
|
// TODO: not sure yet where to get this info. Assuming 24 bits for now.
|
|
int bitsPerPixel = 24;
|
|
return new ImageInfo(new PixelTypeInfo(bitsPerPixel), imageInfo.Width, imageInfo.Height, this.metadata);
|
|
}
|
|
|
|
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 = 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();
|
|
}
|
|
|
|
private WebPImageInfo ReadVp8XHeader()
|
|
{
|
|
uint chunkSize = this.ReadChunkSize();
|
|
|
|
// This byte contains information about the image features used.
|
|
// The first two bit should and the last bit 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;
|
|
int width = 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;
|
|
|
|
// Optional chunks ALPH, ICCP and ANIM can follow here. Ignoring them for now.
|
|
WebPChunkType chunkType;
|
|
if (isIccPresent)
|
|
{
|
|
chunkType = this.ReadChunkType();
|
|
uint iccpChunkSize = this.ReadChunkSize();
|
|
this.currentStream.Skip((int)iccpChunkSize);
|
|
}
|
|
|
|
if (isAnimationPresent)
|
|
{
|
|
this.webpMetadata.Animated = true;
|
|
|
|
return new WebPImageInfo()
|
|
{
|
|
Width = width,
|
|
Height = height,
|
|
IsAnimation = true
|
|
};
|
|
}
|
|
|
|
if (isAlphaPresent)
|
|
{
|
|
chunkType = this.ReadChunkType();
|
|
uint alphaChunkSize = this.ReadChunkSize();
|
|
this.currentStream.Skip((int)alphaChunkSize);
|
|
}
|
|
|
|
// 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.
|
|
switch (chunkType)
|
|
{
|
|
case WebPChunkType.Vp8:
|
|
return this.ReadVp8Header();
|
|
case WebPChunkType.Vp8L:
|
|
return this.ReadVp8LHeader();
|
|
}
|
|
|
|
WebPThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
|
|
|
|
return new WebPImageInfo();
|
|
}
|
|
|
|
private WebPImageInfo ReadVp8Header()
|
|
{
|
|
this.webpMetadata.Format = WebPFormatType.Lossy;
|
|
|
|
// VP8 data size.
|
|
this.currentStream.Read(this.buffer, 0, 3);
|
|
this.buffer[3] = 0;
|
|
uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer);
|
|
|
|
// 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);
|
|
int tmp = (this.buffer[2] << 16) | (this.buffer[1] << 8) | this.buffer[0];
|
|
int isKeyFrame = tmp & 0x1;
|
|
int version = (tmp >> 1) & 0x7;
|
|
int showFrame = (tmp >> 4) & 0x1;
|
|
|
|
// Check for VP8 magic bytes.
|
|
this.currentStream.Read(this.buffer, 0, 4);
|
|
if (!this.buffer.AsSpan(1).SequenceEqual(WebPConstants.Vp8MagicBytes))
|
|
{
|
|
WebPThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
|
|
}
|
|
|
|
this.currentStream.Read(this.buffer, 0, 4);
|
|
|
|
// TODO: Get horizontal and vertical scale
|
|
int width = BinaryPrimitives.ReadInt16LittleEndian(this.buffer) & 0x3fff;
|
|
int height = BinaryPrimitives.ReadInt16LittleEndian(this.buffer.AsSpan(2)) & 0x3fff;
|
|
|
|
return new WebPImageInfo()
|
|
{
|
|
Width = width,
|
|
Height = height,
|
|
IsLossLess = false,
|
|
ImageDataSize = dataSize
|
|
};
|
|
}
|
|
|
|
private WebPImageInfo ReadVp8LHeader()
|
|
{
|
|
this.webpMetadata.Format = WebPFormatType.Lossless;
|
|
|
|
// VP8 data size.
|
|
uint dataSize = this.ReadChunkSize();
|
|
|
|
// One byte signature, should be 0x2f.
|
|
byte signature = (byte)this.currentStream.ReadByte();
|
|
if (signature != WebPConstants.Vp8LMagicByte)
|
|
{
|
|
WebPThrowHelper.ThrowImageFormatException("Invalid VP8L signature");
|
|
}
|
|
|
|
// The first 28 bits of the bitstream specify the width and height of the image.
|
|
var bitReader = new Vp8LBitReader(this.currentStream);
|
|
uint width = bitReader.Read(WebPConstants.Vp8LImageSizeBits) + 1;
|
|
uint height = bitReader.Read(WebPConstants.Vp8LImageSizeBits) + 1;
|
|
|
|
// The alpha_is_used flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise.
|
|
bool alphaIsUsed = bitReader.ReadBit();
|
|
|
|
// The next 3 bytes 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.Read(3);
|
|
|
|
return new WebPImageInfo()
|
|
{
|
|
Width = (int)width,
|
|
Height = (int)height,
|
|
IsLossLess = true,
|
|
ImageDataSize = dataSize
|
|
};
|
|
}
|
|
|
|
private void ReadSimpleLossy<TPixel>(Buffer2D<TPixel> pixels, int width, int height, int imageDataSize)
|
|
where TPixel : struct, IPixel<TPixel>
|
|
{
|
|
// TODO: implement decoding. For simulating the decoding: skipping the chunk size bytes.
|
|
this.currentStream.Skip(imageDataSize - 10);
|
|
}
|
|
|
|
private void ReadSimpleLossless<TPixel>(Buffer2D<TPixel> pixels, int width, int height, int imageDataSize)
|
|
where TPixel : struct, IPixel<TPixel>
|
|
{
|
|
var losslessDecoder = new WebPLosslessDecoder(this.currentStream);
|
|
losslessDecoder.Decode(pixels, width, height, imageDataSize);
|
|
|
|
// TODO: implement decoding. For simulating the decoding: skipping the chunk size bytes.
|
|
this.currentStream.Skip(imageDataSize); // TODO: this does not seem to work in all cases
|
|
}
|
|
|
|
private void ReadExtended<TPixel>(Buffer2D<TPixel> pixels, int width, int height)
|
|
where TPixel : struct, IPixel<TPixel>
|
|
{
|
|
// TODO: implement decoding
|
|
}
|
|
|
|
private void ParseOptionalChunks()
|
|
{
|
|
while (this.currentStream.Position < this.currentStream.Length)
|
|
{
|
|
// Read chunk header.
|
|
WebPChunkType chunkType = this.ReadChunkType();
|
|
uint chunkLength = this.ReadChunkSize();
|
|
|
|
// Skip chunk data for now.
|
|
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);
|
|
this.webpMetadata.ChunkTypes.Enqueue(chunkType);
|
|
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.");
|
|
}
|
|
}
|
|
}
|
|
|