mirror of https://github.com/SixLabors/ImageSharp
57 changed files with 1292 additions and 399 deletions
@ -0,0 +1,23 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Webp |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
|
||||
|
/// </summary>
|
||||
|
internal enum AnimationBlendingMethod |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending.
|
||||
|
/// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle.
|
||||
|
/// </summary>
|
||||
|
AlphaBlending = 0, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Do not blend. After disposing of the previous frame,
|
||||
|
/// render the current frame on the canvas by overwriting the rectangle covered by the current frame.
|
||||
|
/// </summary>
|
||||
|
DoNotBlend = 1 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Webp |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
|
||||
|
/// </summary>
|
||||
|
internal enum AnimationDisposalMethod |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Do not dispose. Leave the canvas as is.
|
||||
|
/// </summary>
|
||||
|
DoNotDispose = 0, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk.
|
||||
|
/// </summary>
|
||||
|
Dispose = 1 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Webp |
||||
|
{ |
||||
|
internal struct AnimationFrameData |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The animation chunk size.
|
||||
|
/// </summary>
|
||||
|
public uint DataSize; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The X coordinate of the upper left corner of the frame is Frame X * 2.
|
||||
|
/// </summary>
|
||||
|
public uint X; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The Y coordinate of the upper left corner of the frame is Frame Y * 2.
|
||||
|
/// </summary>
|
||||
|
public uint Y; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The width of the frame.
|
||||
|
/// </summary>
|
||||
|
public uint Width; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The height of the frame.
|
||||
|
/// </summary>
|
||||
|
public uint Height; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The time to wait before displaying the next frame, in 1 millisecond units.
|
||||
|
/// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined.
|
||||
|
/// </summary>
|
||||
|
public uint Duration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
|
||||
|
/// </summary>
|
||||
|
public AnimationBlendingMethod BlendingMethod; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
|
||||
|
/// </summary>
|
||||
|
public AnimationDisposalMethod DisposalMethod; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,386 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
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.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Webp |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Decoder for animated webp images.
|
||||
|
/// </summary>
|
||||
|
internal class WebpAnimationDecoder : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Reusable buffer.
|
||||
|
/// </summary>
|
||||
|
private readonly byte[] buffer = new byte[4]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Used for allocating memory during the decoding operations.
|
||||
|
/// </summary>
|
||||
|
private readonly MemoryAllocator memoryAllocator; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The global configuration.
|
||||
|
/// </summary>
|
||||
|
private readonly Configuration configuration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The area to restore.
|
||||
|
/// </summary>
|
||||
|
private Rectangle? restoreArea; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The abstract metadata.
|
||||
|
/// </summary>
|
||||
|
private ImageMetadata metadata; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The gif specific metadata.
|
||||
|
/// </summary>
|
||||
|
private WebpMetadata webpMetadata; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="memoryAllocator">The memory allocator.</param>
|
||||
|
/// <param name="configuration">The global configuration.</param>
|
||||
|
/// <param name="decodingMode">The frame decoding mode.</param>
|
||||
|
public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, FrameDecodingMode decodingMode) |
||||
|
{ |
||||
|
this.memoryAllocator = memoryAllocator; |
||||
|
this.configuration = configuration; |
||||
|
this.DecodingMode = decodingMode; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the alpha data, if an ALPH chunk is present.
|
||||
|
/// </summary>
|
||||
|
public IMemoryOwner<byte> AlphaData { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the decoding mode for multi-frame images.
|
||||
|
/// </summary>
|
||||
|
public FrameDecodingMode DecodingMode { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Decodes the animated webp image from the specified stream.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
|
||||
|
/// <param name="features">The webp features.</param>
|
||||
|
/// <param name="width">The width of the image.</param>
|
||||
|
/// <param name="height">The height of the image.</param>
|
||||
|
/// <param name="completeDataSize">The size of the image data in bytes.</param>
|
||||
|
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, WebpFeatures features, uint width, uint height, uint completeDataSize) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
Image<TPixel> image = null; |
||||
|
ImageFrame<TPixel> previousFrame = null; |
||||
|
|
||||
|
this.metadata = new ImageMetadata(); |
||||
|
this.webpMetadata = this.metadata.GetWebpMetadata(); |
||||
|
this.webpMetadata.AnimationLoopCount = features.AnimationLoopCount; |
||||
|
|
||||
|
int remainingBytes = (int)completeDataSize; |
||||
|
while (remainingBytes > 0) |
||||
|
{ |
||||
|
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); |
||||
|
remainingBytes -= 4; |
||||
|
switch (chunkType) |
||||
|
{ |
||||
|
case WebpChunkType.Animation: |
||||
|
uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, features.AnimationBackgroundColor.Value); |
||||
|
remainingBytes -= (int)dataSize; |
||||
|
break; |
||||
|
case WebpChunkType.Xmp: |
||||
|
case WebpChunkType.Exif: |
||||
|
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image.Metadata, false, this.buffer); |
||||
|
break; |
||||
|
default: |
||||
|
WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data"); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (stream.Position == stream.Length || this.DecodingMode is FrameDecodingMode.First) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return image; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads an individual webp frame.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
|
||||
|
/// <param name="image">The image to decode the information to.</param>
|
||||
|
/// <param name="previousFrame">The previous frame.</param>
|
||||
|
/// <param name="width">The width of the image.</param>
|
||||
|
/// <param name="height">The height of the image.</param>
|
||||
|
/// <param name="backgroundColor">The default background color of the canvas in.</param>
|
||||
|
private uint ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame, uint width, uint height, Color backgroundColor) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
AnimationFrameData frameData = this.ReadFrameHeader(stream); |
||||
|
long streamStartPosition = stream.Position; |
||||
|
|
||||
|
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); |
||||
|
bool hasAlpha = false; |
||||
|
byte alphaChunkHeader = 0; |
||||
|
if (chunkType is WebpChunkType.Alpha) |
||||
|
{ |
||||
|
alphaChunkHeader = this.ReadAlphaData(stream); |
||||
|
hasAlpha = true; |
||||
|
chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer); |
||||
|
} |
||||
|
|
||||
|
WebpImageInfo webpInfo = null; |
||||
|
var features = new WebpFeatures(); |
||||
|
switch (chunkType) |
||||
|
{ |
||||
|
case WebpChunkType.Vp8: |
||||
|
webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features); |
||||
|
features.Alpha = hasAlpha; |
||||
|
features.AlphaChunkHeader = alphaChunkHeader; |
||||
|
break; |
||||
|
case WebpChunkType.Vp8L: |
||||
|
webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features); |
||||
|
break; |
||||
|
default: |
||||
|
WebpThrowHelper.ThrowImageFormatException("Read unexpected chunk type, should be VP8 or VP8L"); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
ImageFrame<TPixel> currentFrame = null; |
||||
|
ImageFrame<TPixel> imageFrame; |
||||
|
if (previousFrame is null) |
||||
|
{ |
||||
|
image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor.ToPixel<TPixel>(), this.metadata); |
||||
|
|
||||
|
this.SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); |
||||
|
|
||||
|
imageFrame = image.Frames.RootFrame; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
currentFrame = image.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection.
|
||||
|
|
||||
|
this.SetFrameMetadata(currentFrame.Metadata, frameData.Duration); |
||||
|
|
||||
|
imageFrame = currentFrame; |
||||
|
} |
||||
|
|
||||
|
int frameX = (int)(frameData.X * 2); |
||||
|
int frameY = (int)(frameData.Y * 2); |
||||
|
int frameWidth = (int)frameData.Width; |
||||
|
int frameHeight = (int)frameData.Height; |
||||
|
var regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight); |
||||
|
|
||||
|
if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose) |
||||
|
{ |
||||
|
this.RestoreToBackground(imageFrame, backgroundColor); |
||||
|
} |
||||
|
|
||||
|
using Buffer2D<TPixel> decodedImage = this.DecodeImageData<TPixel>(frameData, webpInfo); |
||||
|
this.DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); |
||||
|
|
||||
|
if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending) |
||||
|
{ |
||||
|
this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); |
||||
|
} |
||||
|
|
||||
|
previousFrame = currentFrame ?? image.Frames.RootFrame; |
||||
|
this.restoreArea = regionRectangle; |
||||
|
|
||||
|
return (uint)(stream.Position - streamStartPosition); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the frames metadata.
|
||||
|
/// </summary>
|
||||
|
/// <param name="meta">The metadata.</param>
|
||||
|
/// <param name="duration">The frame duration.</param>
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private void SetFrameMetadata(ImageFrameMetadata meta, uint duration) |
||||
|
{ |
||||
|
WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); |
||||
|
frameMetadata.FrameDuration = duration; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads the ALPH chunk data.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream to read from.</param>
|
||||
|
private byte ReadAlphaData(BufferedReadStream stream) |
||||
|
{ |
||||
|
this.AlphaData?.Dispose(); |
||||
|
|
||||
|
uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer); |
||||
|
int alphaDataSize = (int)(alphaChunkSize - 1); |
||||
|
this.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize); |
||||
|
|
||||
|
byte alphaChunkHeader = (byte)stream.ReadByte(); |
||||
|
Span<byte> alphaData = this.AlphaData.GetSpan(); |
||||
|
stream.Read(alphaData, 0, alphaDataSize); |
||||
|
|
||||
|
return alphaChunkHeader; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Decodes the either lossy or lossless webp image data.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="frameData">The frame data.</param>
|
||||
|
/// <param name="webpInfo">The webp information.</param>
|
||||
|
/// <returns>A decoded image.</returns>
|
||||
|
private Buffer2D<TPixel> DecodeImageData<TPixel>(AnimationFrameData frameData, WebpImageInfo webpInfo) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
var decodedImage = new Image<TPixel>((int)frameData.Width, (int)frameData.Height); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
Buffer2D<TPixel> pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer; |
||||
|
if (webpInfo.IsLossless) |
||||
|
{ |
||||
|
var losslessDecoder = new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); |
||||
|
losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var lossyDecoder = new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); |
||||
|
lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.AlphaData); |
||||
|
} |
||||
|
|
||||
|
return pixelBufferDecoded; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
decodedImage?.Dispose(); |
||||
|
throw; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
webpInfo.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Draws the decoded image on canvas. The decoded image can be smaller the the canvas.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
|
||||
|
/// <param name="decodedImage">The decoded image.</param>
|
||||
|
/// <param name="imageFrame">The image frame to draw into.</param>
|
||||
|
/// <param name="frameX">The frame x coordinate.</param>
|
||||
|
/// <param name="frameY">The frame y coordinate.</param>
|
||||
|
/// <param name="frameWidth">The width of the frame.</param>
|
||||
|
/// <param name="frameHeight">The height of the frame.</param>
|
||||
|
private void DrawDecodedImageOnCanvas<TPixel>(Buffer2D<TPixel> decodedImage, ImageFrame<TPixel> imageFrame, int frameX, int frameY, int frameWidth, int frameHeight) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
Buffer2D<TPixel> imageFramePixels = imageFrame.PixelBuffer; |
||||
|
int decodedRowIdx = 0; |
||||
|
for (int y = frameY; y < frameY + frameHeight; y++) |
||||
|
{ |
||||
|
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y); |
||||
|
Span<TPixel> decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++).Slice(0, frameWidth); |
||||
|
decodedPixelRow.TryCopyTo(framePixelRow.Slice(frameX)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// After disposing of the previous frame, render the current frame on the canvas using alpha-blending.
|
||||
|
/// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="src">The source image.</param>
|
||||
|
/// <param name="dst">The destination image.</param>
|
||||
|
/// <param name="frameX">The frame x coordinate.</param>
|
||||
|
/// <param name="frameY">The frame y coordinate.</param>
|
||||
|
/// <param name="frameWidth">The width of the frame.</param>
|
||||
|
/// <param name="frameHeight">The height of the frame.</param>
|
||||
|
private void AlphaBlend<TPixel>(ImageFrame<TPixel> src, ImageFrame<TPixel> dst, int frameX, int frameY, int frameWidth, int frameHeight) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
Buffer2D<TPixel> srcPixels = src.PixelBuffer; |
||||
|
Buffer2D<TPixel> dstPixels = dst.PixelBuffer; |
||||
|
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); |
||||
|
for (int y = frameY; y < frameY + frameHeight; y++) |
||||
|
{ |
||||
|
Span<TPixel> srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); |
||||
|
Span<TPixel> dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); |
||||
|
|
||||
|
blender.Blend<TPixel>(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Dispose to background color. Fill the rectangle on the canvas covered by the current frame
|
||||
|
/// with background color specified in the ANIM chunk.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="imageFrame">The image frame.</param>
|
||||
|
/// <param name="backgroundColor">Color of the background.</param>
|
||||
|
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> imageFrame, Color backgroundColor) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
if (!this.restoreArea.HasValue) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value); |
||||
|
Buffer2DRegion<TPixel> pixelRegion = imageFrame.PixelBuffer.GetRegion(interest); |
||||
|
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>(); |
||||
|
pixelRegion.Fill(backgroundPixel); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads the animation frame header.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream to read from.</param>
|
||||
|
/// <returns>Animation frame data.</returns>
|
||||
|
private AnimationFrameData ReadFrameHeader(BufferedReadStream stream) |
||||
|
{ |
||||
|
var data = new AnimationFrameData |
||||
|
{ |
||||
|
DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer) |
||||
|
}; |
||||
|
|
||||
|
// 3 bytes for the X coordinate of the upper left corner of the frame.
|
||||
|
data.X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer); |
||||
|
|
||||
|
// 3 bytes for the Y coordinate of the upper left corner of the frame.
|
||||
|
data.Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer); |
||||
|
|
||||
|
// Frame width Minus One.
|
||||
|
data.Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1; |
||||
|
|
||||
|
// Frame height Minus One.
|
||||
|
data.Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1; |
||||
|
|
||||
|
// Frame duration.
|
||||
|
data.Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer); |
||||
|
|
||||
|
byte flags = (byte)stream.ReadByte(); |
||||
|
data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; |
||||
|
data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Dispose() => this.AlphaData?.Dispose(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,375 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers.Binary; |
||||
|
using SixLabors.ImageSharp.Formats.Webp.BitReader; |
||||
|
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.Xmp; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Webp |
||||
|
{ |
||||
|
internal static class WebpChunkParsingUtils |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Reads the header of a lossy webp image.
|
||||
|
/// </summary>
|
||||
|
/// <returns>Information about this webp image.</returns>
|
||||
|
public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features) |
||||
|
{ |
||||
|
// VP8 data size (not including this 4 bytes).
|
||||
|
int bytesRead = stream.Read(buffer, 0, 4); |
||||
|
if (bytesRead != 4) |
||||
|
{ |
||||
|
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header"); |
||||
|
} |
||||
|
|
||||
|
uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(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.
|
||||
|
bytesRead = stream.Read(buffer, 0, 3); |
||||
|
if (bytesRead != 3) |
||||
|
{ |
||||
|
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header"); |
||||
|
} |
||||
|
|
||||
|
uint frameTag = (uint)(buffer[0] | (buffer[1] << 8) | (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.
|
||||
|
bytesRead = stream.Read(buffer, 0, 3); |
||||
|
if (bytesRead != 3) |
||||
|
{ |
||||
|
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); |
||||
|
} |
||||
|
|
||||
|
if (!buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) |
||||
|
{ |
||||
|
WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); |
||||
|
} |
||||
|
|
||||
|
bytesRead = stream.Read(buffer, 0, 4); |
||||
|
if (bytesRead != 4) |
||||
|
{ |
||||
|
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header, could not read width and height"); |
||||
|
} |
||||
|
|
||||
|
uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); |
||||
|
uint width = tmp & 0x3fff; |
||||
|
sbyte xScale = (sbyte)(tmp >> 6); |
||||
|
tmp = BinaryPrimitives.ReadUInt16LittleEndian(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( |
||||
|
stream, |
||||
|
remaining, |
||||
|
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>
|
||||
|
/// <returns>Information about this image.</returns>
|
||||
|
public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features) |
||||
|
{ |
||||
|
// VP8 data size.
|
||||
|
uint imageDataSize = ReadChunkSize(stream, buffer); |
||||
|
|
||||
|
var bitReader = new Vp8LBitReader(stream, imageDataSize, 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>
|
||||
|
/// 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>
|
||||
|
public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, byte[] buffer, WebpFeatures features) |
||||
|
{ |
||||
|
uint fileSize = ReadChunkSize(stream, buffer); |
||||
|
|
||||
|
// The first byte contains information about the image features used.
|
||||
|
byte imageFeatures = (byte)stream.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.
|
||||
|
stream.Read(buffer, 0, 3); |
||||
|
if (buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 0) |
||||
|
{ |
||||
|
WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero"); |
||||
|
} |
||||
|
|
||||
|
// 3 bytes for the width.
|
||||
|
uint width = ReadUnsignedInt24Bit(stream, buffer) + 1; |
||||
|
|
||||
|
// 3 bytes for the height.
|
||||
|
uint height = ReadUnsignedInt24Bit(stream, buffer) + 1; |
||||
|
|
||||
|
// Read all the chunks in the order they occur.
|
||||
|
var info = new WebpImageInfo() |
||||
|
{ |
||||
|
Width = width, |
||||
|
Height = height, |
||||
|
Features = features |
||||
|
}; |
||||
|
|
||||
|
return info; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads a unsigned 24 bit integer.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream to read from.</param>
|
||||
|
/// <param name="buffer">The buffer to store the read data into.</param>
|
||||
|
/// <returns>A unsigned 24 bit integer.</returns>
|
||||
|
public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, byte[] buffer) |
||||
|
{ |
||||
|
if (stream.Read(buffer, 0, 3) == 3) |
||||
|
{ |
||||
|
buffer[3] = 0; |
||||
|
return BinaryPrimitives.ReadUInt32LittleEndian(buffer); |
||||
|
} |
||||
|
|
||||
|
throw new ImageFormatException("Invalid Webp data, could not read unsigned integer."); |
||||
|
} |
||||
|
|
||||
|
/// <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>
|
||||
|
/// <param name="stream">The stream to read the data from.</param>
|
||||
|
/// <param name="buffer">Buffer to store the data read from the stream.</param>
|
||||
|
/// <returns>The chunk size in bytes.</returns>
|
||||
|
public static uint ReadChunkSize(BufferedReadStream stream, byte[] buffer) |
||||
|
{ |
||||
|
if (stream.Read(buffer, 0, 4) == 4) |
||||
|
{ |
||||
|
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); |
||||
|
return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; |
||||
|
} |
||||
|
|
||||
|
throw new ImageFormatException("Invalid Webp data, could not read chunk size."); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Identifies the chunk type from the chunk.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream to read the data from.</param>
|
||||
|
/// <param name="buffer">Buffer to store the data read from the stream.</param>
|
||||
|
/// <exception cref="ImageFormatException">
|
||||
|
/// Thrown if the input stream is not valid.
|
||||
|
/// </exception>
|
||||
|
public static WebpChunkType ReadChunkType(BufferedReadStream stream, byte[] buffer) |
||||
|
{ |
||||
|
if (stream.Read(buffer, 0, 4) == 4) |
||||
|
{ |
||||
|
var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); |
||||
|
return chunkType; |
||||
|
} |
||||
|
|
||||
|
throw new ImageFormatException("Invalid Webp data, could not read chunk type."); |
||||
|
} |
||||
|
|
||||
|
/// <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>
|
||||
|
public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, byte[] buffer) |
||||
|
{ |
||||
|
long streamLength = stream.Length; |
||||
|
while (stream.Position < streamLength) |
||||
|
{ |
||||
|
uint chunkLength = ReadChunkSize(stream, buffer); |
||||
|
|
||||
|
if (ignoreMetaData) |
||||
|
{ |
||||
|
stream.Skip((int)chunkLength); |
||||
|
} |
||||
|
|
||||
|
int bytesRead; |
||||
|
switch (chunkType) |
||||
|
{ |
||||
|
case WebpChunkType.Exif: |
||||
|
byte[] exifData = new byte[chunkLength]; |
||||
|
bytesRead = stream.Read(exifData, 0, (int)chunkLength); |
||||
|
if (bytesRead != chunkLength) |
||||
|
{ |
||||
|
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile"); |
||||
|
} |
||||
|
|
||||
|
if (metadata.ExifProfile != null) |
||||
|
{ |
||||
|
metadata.ExifProfile = new ExifProfile(exifData); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case WebpChunkType.Xmp: |
||||
|
byte[] xmpData = new byte[chunkLength]; |
||||
|
bytesRead = stream.Read(xmpData, 0, (int)chunkLength); |
||||
|
if (bytesRead != chunkLength) |
||||
|
{ |
||||
|
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile"); |
||||
|
} |
||||
|
|
||||
|
if (metadata.XmpProfile != null) |
||||
|
{ |
||||
|
metadata.XmpProfile = new XmpProfile(xmpData); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
default: |
||||
|
stream.Skip((int)chunkLength); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <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>
|
||||
|
public static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch |
||||
|
{ |
||||
|
WebpChunkType.Alpha => true, |
||||
|
WebpChunkType.AnimationParameter => true, |
||||
|
WebpChunkType.Exif => true, |
||||
|
WebpChunkType.Iccp => true, |
||||
|
WebpChunkType.Xmp => true, |
||||
|
_ => false |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Webp |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides webp specific metadata information for the image frame.
|
||||
|
/// </summary>
|
||||
|
public class WebpFrameMetadata : IDeepCloneable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="WebpFrameMetadata"/> class.
|
||||
|
/// </summary>
|
||||
|
public WebpFrameMetadata() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="WebpFrameMetadata"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="other">The metadata to create an instance from.</param>
|
||||
|
private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the frame duration. The time to wait before displaying the next frame,
|
||||
|
/// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined.
|
||||
|
/// </summary>
|
||||
|
public uint FrameDuration { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public IDeepCloneable DeepClone() => new WebpFrameMetadata(this); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:d99914f1a4dc3e554b9dded9e547194685b1b9ecc5d816d9f329cef483c525d5 |
||||
|
size 50298 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:391ed80dc5ba4a21bdc4ea4db9fde4c6dad8556d1b8f0bf198db3c2bb5dc50ad |
||||
|
size 49389 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:84bf215392014c2d7dbeb495bd1717bc2da4566b285bc388ed7bc8e88ebb0e85 |
||||
|
size 52686 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:0e7a47ba473440f699f337fb8886cd170c6610452b3145c068a0f18584541559 |
||||
|
size 53244 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:a4e7572c91c73e63e74c795e16ce951fbbdba5a015921102844d7bdf0fb0b473 |
||||
|
size 56046 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:6681af3640adb85452f9c1fa0cb5dce04638b48d80994c20c40d11e07670f1de |
||||
|
size 62469 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:8549aeb786fc12d4e947b3b5f862701fab8158576193a03877f4b891815077e0 |
||||
|
size 61068 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:474a6bbf07604de5a412d1eed2d3ba6ce191a85b88464c5848a50bef42566de5 |
||||
|
size 60411 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:f296bbd4b5637d1583ea337e8b807b34613640e0eabfb5e13e4e6cefe8ae2527 |
||||
|
size 58793 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:b16c16f9663b5ba80fa2ef06503851009b15700ff257375bd41cdb362098a391 |
||||
|
size 57157 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:b5c39781b77219a6e9c05233d2376dfde04bd0dbe39f63274168073abf7a0e4d |
||||
|
size 55424 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:5133dc9a5f8f6d26d388f40fd1df3a262f489d80a0d1eed588f7662bef7523de |
||||
|
size 59950 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:d99914f1a4dc3e554b9dded9e547194685b1b9ecc5d816d9f329cef483c525d5 |
||||
|
size 50298 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:391ed80dc5ba4a21bdc4ea4db9fde4c6dad8556d1b8f0bf198db3c2bb5dc50ad |
||||
|
size 49389 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:84bf215392014c2d7dbeb495bd1717bc2da4566b285bc388ed7bc8e88ebb0e85 |
||||
|
size 52686 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:0e7a47ba473440f699f337fb8886cd170c6610452b3145c068a0f18584541559 |
||||
|
size 53244 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:a4e7572c91c73e63e74c795e16ce951fbbdba5a015921102844d7bdf0fb0b473 |
||||
|
size 56046 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:6681af3640adb85452f9c1fa0cb5dce04638b48d80994c20c40d11e07670f1de |
||||
|
size 62469 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:8549aeb786fc12d4e947b3b5f862701fab8158576193a03877f4b891815077e0 |
||||
|
size 61068 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:474a6bbf07604de5a412d1eed2d3ba6ce191a85b88464c5848a50bef42566de5 |
||||
|
size 60411 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:f296bbd4b5637d1583ea337e8b807b34613640e0eabfb5e13e4e6cefe8ae2527 |
||||
|
size 58793 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:b16c16f9663b5ba80fa2ef06503851009b15700ff257375bd41cdb362098a391 |
||||
|
size 57157 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:b5c39781b77219a6e9c05233d2376dfde04bd0dbe39f63274168073abf7a0e4d |
||||
|
size 55424 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:5133dc9a5f8f6d26d388f40fd1df3a262f489d80a0d1eed588f7662bef7523de |
||||
|
size 59950 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:bab815db08e8f413c7a355b7e9c152e1a73e503392012af16ada92858706d255 |
||||
|
size 400342 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:00fffbb0d67b0336574d9bad9cbacaf97d81f2e70db3d458508c430e3d103228 |
||||
|
size 64972 |
||||
Loading…
Reference in new issue