Browse Source

Merge branch 'main' into dp/jpeg-downscaling-decode

pull/2076/head
Brian Popow 4 years ago
committed by GitHub
parent
commit
2896faf697
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 37
      src/ImageSharp/Formats/Bmp/BmpColorSpace.cs
  2. 53
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  3. 150
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  4. 5
      src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
  5. 133
      src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
  6. 37
      src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs
  7. 2
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  8. 8
      src/ImageSharp/Formats/Gif/GifFormat.cs
  9. 101
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  10. 81
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  11. 23
      src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs
  12. 21
      src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs
  13. 49
      src/ImageSharp/Formats/Webp/AnimationFrameData.cs
  14. 36
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  15. 26
      src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
  16. 26
      src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
  17. 7
      src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs
  18. 2
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  19. 2
      src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
  20. 1
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  21. 13
      src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
  22. 7
      src/ImageSharp/Formats/Webp/MetadataExtensions.cs
  23. 386
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  24. 375
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  25. 9
      src/ImageSharp/Formats/Webp/WebpDecoder.cs
  26. 456
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  27. 22
      src/ImageSharp/Formats/Webp/WebpFeatures.cs
  28. 11
      src/ImageSharp/Formats/Webp/WebpFormat.cs
  29. 33
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  30. 1
      src/ImageSharp/Formats/Webp/WebpImageInfo.cs
  31. 11
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  32. 23
      src/ImageSharp/Memory/Buffer2DRegion{T}.cs
  33. 11
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
  34. 4
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  35. 2
      src/ImageSharp/Metadata/ImageMetadata.cs
  36. 28
      src/ImageSharp/Primitives/Rectangle.cs
  37. 4
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  38. 27
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  39. 16
      tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs
  40. 2
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  41. 1
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
  42. 29
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  43. 57
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  44. 2
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  45. 59
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  46. 6
      tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
  47. 15
      tests/ImageSharp.Tests/TestImages.cs
  48. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png
  49. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png
  50. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png
  51. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png
  52. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png
  53. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png
  54. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png
  55. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png
  56. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png
  57. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png
  58. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png
  59. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png
  60. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png
  61. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png
  62. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png
  63. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png
  64. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png
  65. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png
  66. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png
  67. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png
  68. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png
  69. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png
  70. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png
  71. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png
  72. 3
      tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp
  73. 0
      tests/Images/Input/Webp/bike_lossy_small.webp
  74. 0
      tests/Images/Input/Webp/bike_lossy_with_exif.webp
  75. 3
      tests/Images/Input/Webp/leo_animated_lossless.webp
  76. 3
      tests/Images/Input/Webp/leo_animated_lossy.webp

37
src/ImageSharp/Formats/Bmp/BmpColorSpace.cs

@ -0,0 +1,37 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Bmp
{
/// <summary>
/// Enum for the different color spaces.
/// </summary>
internal enum BmpColorSpace
{
/// <summary>
/// This value implies that endpoints and gamma values are given in the appropriate fields.
/// </summary>
LCS_CALIBRATED_RGB = 0,
/// <summary>
/// The Windows default color space ('Win ').
/// </summary>
LCS_WINDOWS_COLOR_SPACE = 1466527264,
/// <summary>
/// Specifies that the bitmap is in sRGB color space ('sRGB').
/// </summary>
LCS_sRGB = 1934772034,
/// <summary>
/// This value indicates that bV5ProfileData points to the file name of the profile to use (gamma and endpoints values are ignored).
/// </summary>
PROFILE_LINKED = 1279872587,
/// <summary>
/// This value indicates that bV5ProfileData points to a memory buffer that contains the profile to be used (gamma and endpoints values are ignored).
/// </summary>
PROFILE_EMBEDDED = 1296188740
}
}

53
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Bmp
@ -185,7 +186,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
break;
default:
BmpThrowHelper.ThrowNotSupportedException("Does not support this kind of bitmap files.");
BmpThrowHelper.ThrowNotSupportedException("ImageSharp does not support this kind of bitmap files.");
break;
}
@ -1199,6 +1200,13 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void ReadInfoHeader()
{
Span<byte> buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize];
long infoHeaderStart = this.stream.Position;
// Resolution is stored in PPM.
this.metadata = new ImageMetadata
{
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
};
// Read the header size.
this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);
@ -1271,36 +1279,45 @@ namespace SixLabors.ImageSharp.Formats.Bmp
infoHeaderType = BmpInfoHeaderType.Os2Version2;
this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer);
}
else if (headerSize >= BmpInfoHeader.SizeV4)
else if (headerSize == BmpInfoHeader.SizeV4)
{
// >= 108 bytes
infoHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5;
// == 108 bytes
infoHeaderType = BmpInfoHeaderType.WinVersion4;
this.infoHeader = BmpInfoHeader.ParseV4(buffer);
}
else if (headerSize > BmpInfoHeader.SizeV4)
{
// > 108 bytes
infoHeaderType = BmpInfoHeaderType.WinVersion5;
this.infoHeader = BmpInfoHeader.ParseV5(buffer);
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
{
// Read color profile.
long streamPosition = this.stream.Position;
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
this.stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
this.stream.Read(iccProfileData);
this.metadata.IccProfile = new IccProfile(iccProfileData);
this.stream.Position = streamPosition;
}
}
else
{
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'.");
}
// Resolution is stored in PPM.
var meta = new ImageMetadata
{
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
};
if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0)
{
meta.HorizontalResolution = this.infoHeader.XPelsPerMeter;
meta.VerticalResolution = this.infoHeader.YPelsPerMeter;
this.metadata.HorizontalResolution = this.infoHeader.XPelsPerMeter;
this.metadata.VerticalResolution = this.infoHeader.YPelsPerMeter;
}
else
{
// Convert default metadata values to PPM.
meta.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution));
meta.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution));
this.metadata.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution));
this.metadata.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution));
}
this.metadata = meta;
short bitsPerPixel = this.infoHeader.BitsPerPixel;
this.bmpMetadata = this.metadata.GetBmpMetadata();
this.bmpMetadata.InfoHeaderType = infoHeaderType;
@ -1370,9 +1387,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int colorMapSizeBytes = -1;
if (this.infoHeader.ClrUsed == 0)
{
if (this.infoHeader.BitsPerPixel == 1
|| this.infoHeader.BitsPerPixel == 4
|| this.infoHeader.BitsPerPixel == 8)
if (this.infoHeader.BitsPerPixel is 1 or 4 or 8)
{
switch (this.fileMarkerType)
{
@ -1424,7 +1439,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int skipAmount = this.fileHeader.Offset - (int)this.stream.Position;
if ((skipAmount + (int)this.stream.Position) > this.stream.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException("Invalid fileheader offset found. Offset is greater than the stream length.");
BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length.");
}
if (skipAmount > 0)

150
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -3,6 +3,7 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
@ -79,9 +80,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary>
/// A bitmap v4 header will only be written, if the user explicitly wants support for transparency.
/// In this case the compression type BITFIELDS will be used.
/// If the image contains a color profile, a bitmap v5 header is written, which is needed to write this info.
/// Otherwise a bitmap v3 header will be written, which is supported by almost all decoders.
/// </summary>
private readonly bool writeV4Header;
private BmpInfoHeaderType infoHeaderType;
/// <summary>
/// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images.
@ -97,8 +99,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel;
this.writeV4Header = options.SupportTransparency;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
}
/// <summary>
@ -123,7 +125,62 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32);
this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F));
// Set Resolution.
int colorPaletteSize = 0;
if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8)
{
colorPaletteSize = ColorPaletteSize8Bit;
}
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4)
{
colorPaletteSize = ColorPaletteSize4Bit;
}
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1)
{
colorPaletteSize = ColorPaletteSize1Bit;
}
byte[] iccProfileData = null;
int iccProfileSize = 0;
if (metadata.IccProfile != null)
{
this.infoHeaderType = BmpInfoHeaderType.WinVersion5;
iccProfileData = metadata.IccProfile.ToByteArray();
iccProfileSize = iccProfileData.Length;
}
int infoHeaderSize = this.infoHeaderType switch
{
BmpInfoHeaderType.WinVersion3 => BmpInfoHeader.SizeV3,
BmpInfoHeaderType.WinVersion4 => BmpInfoHeader.SizeV4,
BmpInfoHeaderType.WinVersion5 => BmpInfoHeader.SizeV5,
_ => BmpInfoHeader.SizeV3
};
BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, image.Height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
Span<byte> buffer = stackalloc byte[infoHeaderSize];
this.WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(stream, image.Frames.RootFrame);
this.WriteColorProfile(stream, iccProfileData, buffer);
stream.Flush();
}
/// <summary>
/// Creates the bitmap information header.
/// </summary>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="infoHeaderSize">Size of the information header.</param>
/// <param name="bpp">The bits per pixel.</param>
/// <param name="bytesPerLine">The bytes per line.</param>
/// <param name="metadata">The metadata.</param>
/// <param name="iccProfileData">The icc profile data.</param>
/// <returns>The bitmap information header.</returns>
private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, short bpp, int bytesPerLine, ImageMetadata metadata, byte[] iccProfileData)
{
int hResolution = 0;
int vResolution = 0;
@ -154,20 +211,19 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
}
int infoHeaderSize = this.writeV4Header ? BmpInfoHeader.SizeV4 : BmpInfoHeader.SizeV3;
var infoHeader = new BmpInfoHeader(
headerSize: infoHeaderSize,
height: image.Height,
width: image.Width,
height: height,
width: width,
bitsPerPixel: bpp,
planes: 1,
imageSize: image.Height * bytesPerLine,
imageSize: height * bytesPerLine,
clrUsed: 0,
clrImportant: 0,
xPelsPerMeter: hResolution,
yPelsPerMeter: vResolution);
if (this.writeV4Header && this.bitsPerPixel == BmpBitsPerPixel.Pixel32)
if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Pixel32)
{
infoHeader.AlphaMask = Rgba32AlphaMask;
infoHeader.RedMask = Rgba32RedMask;
@ -176,45 +232,79 @@ namespace SixLabors.ImageSharp.Formats.Bmp
infoHeader.Compression = BmpCompression.BitFields;
}
int colorPaletteSize = 0;
if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8)
if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && metadata.IccProfile != null)
{
colorPaletteSize = ColorPaletteSize8Bit;
infoHeader.ProfileSize = iccProfileData.Length;
infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED;
infoHeader.Intent = BmpRenderingIntent.LCS_GM_IMAGES;
}
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4)
{
colorPaletteSize = ColorPaletteSize4Bit;
}
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1)
return infoHeader;
}
/// <summary>
/// Writes the color profile to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileData">The color profile data.</param>
/// <param name="buffer">The buffer.</param>
private void WriteColorProfile(Stream stream, byte[] iccProfileData, Span<byte> buffer)
{
if (iccProfileData != null)
{
colorPaletteSize = ColorPaletteSize1Bit;
// The offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data.
int streamPositionAfterImageData = (int)stream.Position - BmpFileHeader.Size;
stream.Write(iccProfileData);
BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData);
stream.Position = BmpFileHeader.Size + 112;
stream.Write(buffer.Slice(0, 4));
}
}
/// <summary>
/// Writes the bitmap file header.
/// </summary>
/// <param name="stream">The stream to write the header to.</param>
/// <param name="infoHeaderSize">Size of the bitmap information header.</param>
/// <param name="colorPaletteSize">Size of the color palette.</param>
/// <param name="iccProfileSize">The size in bytes of the color profile.</param>
/// <param name="infoHeader">The information header to write.</param>
/// <param name="buffer">The buffer to write to.</param>
private void WriteBitmapFileHeader(Stream stream, int infoHeaderSize, int colorPaletteSize, int iccProfileSize, BmpInfoHeader infoHeader, Span<byte> buffer)
{
var fileHeader = new BmpFileHeader(
type: BmpConstants.TypeMarkers.Bitmap,
fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + infoHeader.ImageSize,
fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + iccProfileSize + infoHeader.ImageSize,
reserved: 0,
offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize);
Span<byte> buffer = stackalloc byte[infoHeaderSize];
fileHeader.WriteTo(buffer);
stream.Write(buffer, 0, BmpFileHeader.Size);
}
if (this.writeV4Header)
{
infoHeader.WriteV4Header(buffer);
}
else
/// <summary>
/// Writes the bitmap information header.
/// </summary>
/// <param name="stream">The stream to write info header into.</param>
/// <param name="infoHeader">The information header.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="infoHeaderSize">Size of the information header.</param>
private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span<byte> buffer, int infoHeaderSize)
{
switch (this.infoHeaderType)
{
infoHeader.WriteV3Header(buffer);
case BmpInfoHeaderType.WinVersion3:
infoHeader.WriteV3Header(buffer);
break;
case BmpInfoHeaderType.WinVersion4:
infoHeader.WriteV4Header(buffer);
break;
case BmpInfoHeaderType.WinVersion5:
infoHeader.WriteV5Header(buffer);
break;
}
stream.Write(buffer, 0, infoHeaderSize);
this.WriteImage(stream, image.Frames.RootFrame);
stream.Flush();
}
/// <summary>

5
src/ImageSharp/Formats/Bmp/BmpFileHeader.cs

@ -57,10 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
public int Offset { get; }
public static BmpFileHeader Parse(Span<byte> data)
{
return MemoryMarshal.Cast<byte, BmpFileHeader>(data)[0];
}
public static BmpFileHeader Parse(Span<byte> data) => MemoryMarshal.Cast<byte, BmpFileHeader>(data)[0];
public void WriteTo(Span<byte> buffer)
{

133
src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs

@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int greenMask = 0,
int blueMask = 0,
int alphaMask = 0,
int csType = 0,
BmpColorSpace csType = 0,
int redX = 0,
int redY = 0,
int redZ = 0,
@ -94,7 +94,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int blueZ = 0,
int gammeRed = 0,
int gammeGreen = 0,
int gammeBlue = 0)
int gammeBlue = 0,
BmpRenderingIntent intent = BmpRenderingIntent.Invalid,
int profileData = 0,
int profileSize = 0,
int reserved = 0)
{
this.HeaderSize = headerSize;
this.Width = width;
@ -124,6 +128,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.GammaRed = gammeRed;
this.GammaGreen = gammeGreen;
this.GammaBlue = gammeBlue;
this.Intent = intent;
this.ProfileData = profileData;
this.ProfileSize = profileSize;
this.Reserved = reserved;
}
/// <summary>
@ -211,7 +219,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary>
/// Gets or sets the Color space type. Not used yet.
/// </summary>
public int CsType { get; set; }
public BmpColorSpace CsType { get; set; }
/// <summary>
/// Gets or sets the X coordinate of red endpoint. Not used yet.
@ -273,21 +281,38 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
public int GammaBlue { get; set; }
/// <summary>
/// Gets or sets the rendering intent for bitmap.
/// </summary>
public BmpRenderingIntent Intent { get; set; }
/// <summary>
/// Gets or sets the offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data.
/// </summary>
public int ProfileData { get; set; }
/// <summary>
/// Gets or sets the size, in bytes, of embedded profile data.
/// </summary>
public int ProfileSize { get; set; }
/// <summary>
/// Gets or sets the reserved value.
/// </summary>
public int Reserved { get; set; }
/// <summary>
/// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns>
/// <seealso href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183372.aspx"/>
public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data)
{
return new BmpInfoHeader(
public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data) => new(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(4, 2)),
height: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(6, 2)),
planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(8, 2)),
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(10, 2)));
}
/// <summary>
/// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height
@ -296,15 +321,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns>
/// <seealso href="https://www.fileformat.info/format/os2bmp/egff.htm"/>
public static BmpInfoHeader ParseOs22Short(ReadOnlySpan<byte> data)
{
return new BmpInfoHeader(
public static BmpInfoHeader ParseOs22Short(ReadOnlySpan<byte> data) => new(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)),
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)));
}
/// <summary>
/// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes).
@ -312,9 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns>
/// <seealso href="http://www.fileformat.info/format/bmp/egff.htm"/>
public static BmpInfoHeader ParseV3(ReadOnlySpan<byte> data)
{
return new BmpInfoHeader(
public static BmpInfoHeader ParseV3(ReadOnlySpan<byte> data) => new(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
@ -326,7 +346,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)),
clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)),
clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)));
}
/// <summary>
/// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it.
@ -336,9 +355,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="withAlpha">Indicates, if the alpha bitmask is present.</param>
/// <returns>The parsed header.</returns>
/// <seealso href="https://forums.adobe.com/message/3272950#3272950"/>
public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan<byte> data, bool withAlpha = true)
{
return new BmpInfoHeader(
public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan<byte> data, bool withAlpha = true) => new(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
@ -354,7 +371,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)),
blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)),
alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0);
}
/// <summary>
/// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are
@ -413,11 +429,47 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns>
/// <seealso href="http://www.fileformat.info/format/bmp/egff.htm"/>
public static BmpInfoHeader ParseV4(ReadOnlySpan<byte> data)
public static BmpInfoHeader ParseV4(ReadOnlySpan<byte> data) => new(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)),
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)),
compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)),
imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)),
xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)),
yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)),
clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)),
clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)),
redMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(40, 4)),
greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)),
blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)),
alphaMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)),
csType: (BmpColorSpace)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(56, 4)),
redX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(60, 4)),
redY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(64, 4)),
redZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(68, 4)),
greenX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(72, 4)),
greenY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(76, 4)),
greenZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(80, 4)),
blueX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(84, 4)),
blueY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(88, 4)),
blueZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(92, 4)),
gammeRed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(96, 4)),
gammeGreen: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(100, 4)),
gammeBlue: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(104, 4)));
/// <summary>
/// Parses the full BMP Version 5 BITMAPINFOHEADER header (124 bytes).
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns>
/// <seealso href="https://docs.microsoft.com/de-de/windows/win32/api/wingdi/ns-wingdi-bitmapv5header?redirectedfrom=MSDN"/>
public static BmpInfoHeader ParseV5(ReadOnlySpan<byte> data)
{
if (data.Length < SizeV4)
if (data.Length < SizeV5)
{
throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes.");
throw new ArgumentException(nameof(data), $"Must be {SizeV5} bytes. Was {data.Length} bytes.");
}
return MemoryMarshal.Cast<byte, BmpInfoHeader>(data)[0];
@ -448,6 +500,43 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteV4Header(Span<byte> buffer)
{
buffer.Clear();
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(0, 4), SizeV4);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(4, 4), this.Width);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(8, 4), this.Height);
BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(12, 2), this.Planes);
BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(14, 2), this.BitsPerPixel);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(16, 4), (int)this.Compression);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(20, 4), this.ImageSize);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(24, 4), this.XPelsPerMeter);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(28, 4), this.YPelsPerMeter);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(32, 4), this.ClrUsed);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(36, 4), this.ClrImportant);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(40, 4), this.RedMask);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(44, 4), this.GreenMask);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(48, 4), this.BlueMask);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(52, 4), this.AlphaMask);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(56, 4), (int)this.CsType);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(60, 4), this.RedX);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(64, 4), this.RedY);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(68, 4), this.RedZ);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(72, 4), this.GreenX);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(76, 4), this.GreenY);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(80, 4), this.GreenZ);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(84, 4), this.BlueX);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(88, 4), this.BlueY);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(92, 4), this.BlueZ);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(96, 4), this.GammaRed);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(100, 4), this.GammaGreen);
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(104, 4), this.GammaBlue);
}
/// <summary>
/// Writes a complete Bitmap V5 header to a buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteV5Header(Span<byte> buffer)
{
ref BmpInfoHeader dest = ref Unsafe.As<byte, BmpInfoHeader>(ref MemoryMarshal.GetReference(buffer));

37
src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs

@ -0,0 +1,37 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Bmp
{
/// <summary>
/// Enum for the different rendering intent's.
/// </summary>
internal enum BmpRenderingIntent
{
/// <summary>
/// Invalid default value.
/// </summary>
Invalid = 0,
/// <summary>
/// Maintains saturation. Used for business charts and other situations in which undithered colors are required.
/// </summary>
LCS_GM_BUSINESS = 1,
/// <summary>
/// Maintains colorimetric match. Used for graphic designs and named colors.
/// </summary>
LCS_GM_GRAPHICS = 2,
/// <summary>
/// Maintains contrast. Used for photographs and natural images.
/// </summary>
LCS_GM_IMAGES = 4,
/// <summary>
/// Maintains the white point. Matches the colors to their nearest color in the destination gamut.
/// </summary>
LCS_GM_ABS_COLORIMETRIC = 8,
}
}

2
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Gets the dimensions of the image.
/// </summary>
public Size Dimensions => new Size(this.imageDescriptor.Width, this.imageDescriptor.Height);
public Size Dimensions => new(this.imageDescriptor.Width, this.imageDescriptor.Height);
private MemoryAllocator MemoryAllocator => this.Configuration.MemoryAllocator;

8
src/ImageSharp/Formats/Gif/GifFormat.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Gets the current instance.
/// </summary>
public static GifFormat Instance { get; } = new GifFormat();
public static GifFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "GIF";
@ -32,9 +32,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
public IEnumerable<string> FileExtensions => GifConstants.FileExtensions;
/// <inheritdoc/>
public GifMetadata CreateDefaultFormatMetadata() => new GifMetadata();
public GifMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public GifFrameMetadata CreateDefaultFormatFrameMetadata() => new GifFrameMetadata();
public GifFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}
}

101
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -19,6 +19,7 @@ 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;
@ -205,6 +206,9 @@ namespace SixLabors.ImageSharp.Formats.Png
this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true);
}
break;
case PngChunkType.EmbeddedColorProfile:
this.ReadColorProfileChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.End:
goto EOF;
@ -1174,6 +1178,76 @@ namespace SixLabors.ImageSharp.Formats.Png
return true;
}
/// <summary>
/// Reads the color profile chunk. The data is stored similar to the zTXt chunk.
/// </summary>
/// <param name="metadata">The metadata.</param>
/// <param name="data">The bytes containing the profile.</param>
private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan<byte> data)
{
int zeroIndex = data.IndexOf((byte)0);
if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength)
{
return;
}
byte compressionMethod = data[zeroIndex + 1];
if (compressionMethod != 0)
{
// Only compression method 0 is supported (zlib datastream with deflate compression).
return;
}
ReadOnlySpan<byte> keywordBytes = data.Slice(0, zeroIndex);
if (!this.TryReadTextKeyword(keywordBytes, out string name))
{
return;
}
ReadOnlySpan<byte> compressedData = data.Slice(zeroIndex + 2);
if (this.TryUncompressZlibData(compressedData, out byte[] iccpProfileBytes))
{
metadata.IccProfile = new IccProfile(iccpProfileBytes);
}
}
/// <summary>
/// Tries to un-compress zlib compressed data.
/// </summary>
/// <param name="compressedData">The compressed data.</param>
/// <param name="uncompressedBytesArray">The uncompressed bytes array.</param>
/// <returns>True, if de-compressing was successful.</returns>
private unsafe bool TryUncompressZlibData(ReadOnlySpan<byte> compressedData, out byte[] uncompressedBytesArray)
{
fixed (byte* compressedDataBase = compressedData)
{
using (IMemoryOwner<byte> destBuffer = this.memoryAllocator.Allocate<byte>(this.Configuration.StreamProcessingBufferSize))
using (var memoryStreamOutput = new MemoryStream(compressedData.Length))
using (var memoryStreamInput = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length))
using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStreamInput))
using (var inflateStream = new ZlibInflateStream(bufferedStream))
{
Span<byte> destUncompressedData = destBuffer.GetSpan();
if (!inflateStream.AllocateNewBytes(compressedData.Length, false))
{
uncompressedBytesArray = Array.Empty<byte>();
return false;
}
int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length);
while (bytesRead != 0)
{
memoryStreamOutput.Write(destUncompressedData.Slice(0, bytesRead));
bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length);
}
uncompressedBytesArray = memoryStreamOutput.ToArray();
return true;
}
}
}
/// <summary>
/// Compares two ReadOnlySpan&lt;char&gt;s in a case-insensitive method.
/// This is only needed because older frameworks are missing the extension method.
@ -1306,7 +1380,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
else if (this.IsXmpTextData(keywordBytes))
{
XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray());
var xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray());
metadata.XmpProfile = xmpProfile;
}
else
@ -1325,29 +1399,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <returns>The <see cref="bool"/>.</returns>
private bool TryUncompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding, out string value)
{
using (var memoryStream = new MemoryStream(compressedData.ToArray()))
using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream))
using (var inflateStream = new ZlibInflateStream(bufferedStream))
if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData))
{
if (!inflateStream.AllocateNewBytes(compressedData.Length, false))
{
value = null;
return false;
}
var uncompressedBytes = new List<byte>();
// Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here.
int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length);
while (bytesRead != 0)
{
uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray());
bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length);
}
value = encoding.GetString(uncompressedBytes.ToArray());
value = encoding.GetString(uncompressedData);
return true;
}
value = null;
return false;
}
/// <summary>

81
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -87,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
private IMemoryOwner<byte> currentScanline;
/// <summary>
/// The color profile name.
/// </summary>
private const string ColorProfileName = "ICC Profile";
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoderCore" /> class.
/// </summary>
@ -134,6 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
this.WriteColorProfileChunk(stream, metadata);
this.WritePaletteChunk(stream, quantized);
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
@ -656,7 +662,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
/// <summary>
/// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata.
/// Writes an iTXT chunk, containing the XMP metadata to the stream, if such profile is present in the metadata.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="meta">The image metadata.</param>
@ -673,7 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Png
return;
}
var xmpData = meta.XmpProfile.Data;
byte[] xmpData = meta.XmpProfile.Data;
if (xmpData.Length == 0)
{
@ -687,19 +693,49 @@ namespace SixLabors.ImageSharp.Formats.Png
PngConstants.XmpKeyword.CopyTo(payload);
int bytesWritten = PngConstants.XmpKeyword.Length;
// Write the iTxt header (all zeros in this case)
payload[bytesWritten++] = 0;
payload[bytesWritten++] = 0;
payload[bytesWritten++] = 0;
payload[bytesWritten++] = 0;
payload[bytesWritten++] = 0;
// Write the iTxt header (all zeros in this case).
Span<byte> iTxtHeader = payload.Slice(bytesWritten);
iTxtHeader[4] = 0;
iTxtHeader[3] = 0;
iTxtHeader[2] = 0;
iTxtHeader[1] = 0;
iTxtHeader[0] = 0;
bytesWritten += 5;
// And the XMP data itself
// And the XMP data itself.
xmpData.CopyTo(payload.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.InternationalText, payload);
}
}
/// <summary>
/// Writes the color profile chunk.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="metaData">The image meta data.</param>
private void WriteColorProfileChunk(Stream stream, ImageMetadata metaData)
{
if (metaData.IccProfile is null)
{
return;
}
byte[] iccProfileBytes = metaData.IccProfile.ToByteArray();
byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes);
int payloadLength = ColorProfileName.Length + compressedData.Length + 2;
using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
{
Span<byte> outputBytes = owner.GetSpan();
PngConstants.Encoding.GetBytes(ColorProfileName).CopyTo(outputBytes);
int bytesWritten = ColorProfileName.Length;
outputBytes[bytesWritten++] = 0; // Null separator.
outputBytes[bytesWritten++] = 0; // Compression.
compressedData.CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.EmbeddedColorProfile, outputBytes);
}
}
/// <summary>
/// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk,
/// depending whether the text contains any latin characters or should be compressed.
@ -727,13 +763,12 @@ namespace SixLabors.ImageSharp.Formats.Png
}
}
if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) ||
!string.IsNullOrWhiteSpace(textData.TranslatedKeyword)))
if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)))
{
// Write iTXt chunk.
byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword);
byte[] textBytes = textData.Value.Length > this.options.TextCompressionThreshold
? this.GetCompressedTextBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
: PngConstants.TranslatedEncoding.GetBytes(textData.Value);
byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword);
@ -772,18 +807,17 @@ namespace SixLabors.ImageSharp.Formats.Png
if (textData.Value.Length > this.options.TextCompressionThreshold)
{
// Write zTXt chunk.
byte[] compressedData =
this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value));
byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value));
int payloadLength = textData.Keyword.Length + compressedData.Length + 2;
using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
{
Span<byte> outputBytes = owner.GetSpan();
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
int bytesWritten = textData.Keyword.Length;
outputBytes[bytesWritten++] = 0;
outputBytes[bytesWritten++] = 0;
outputBytes[bytesWritten++] = 0; // Null separator.
outputBytes[bytesWritten++] = 0; // Compression.
compressedData.CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray());
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes);
}
}
else
@ -796,9 +830,8 @@ namespace SixLabors.ImageSharp.Formats.Png
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
int bytesWritten = textData.Keyword.Length;
outputBytes[bytesWritten++] = 0;
PngConstants.Encoding.GetBytes(textData.Value)
.CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray());
PngConstants.Encoding.GetBytes(textData.Value).CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.Text, outputBytes);
}
}
}
@ -808,15 +841,15 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Compresses a given text using Zlib compression.
/// </summary>
/// <param name="textBytes">The text bytes to compress.</param>
/// <returns>The compressed text byte array.</returns>
private byte[] GetCompressedTextBytes(byte[] textBytes)
/// <param name="dataBytes">The bytes to compress.</param>
/// <returns>The compressed byte array.</returns>
private byte[] GetZlibCompressedBytes(byte[] dataBytes)
{
using (var memoryStream = new MemoryStream())
{
using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel))
{
deflateStream.Write(textBytes);
deflateStream.Write(dataBytes);
}
return memoryStream.ToArray();

23
src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs

@ -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
}
}

21
src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs

@ -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
}
}

49
src/ImageSharp/Formats/Webp/AnimationFrameData.cs

@ -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;
}
}

36
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
@ -97,7 +98,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
}
/// <summary>
/// Calculates the chunk size of EXIF or XMP metadata.
/// Calculates the chunk size of EXIF, XMP or ICCP metadata.
/// </summary>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The metadata chunk size in bytes.</returns>
@ -178,16 +179,41 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
}
}
/// <summary>
/// Writes the color profile to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes)
{
uint size = (uint)iccProfileBytes.Length;
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
stream.Write(buf);
stream.Write(iccProfileBytes);
// Add padding byte if needed.
if ((size & 1) == 1)
{
stream.WriteByte(0);
}
}
/// <summary>
/// Writes a VP8X header to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, byte[] iccProfileBytes, uint width, uint height, bool hasAlpha)
{
if (width > MaxDimension || height > MaxDimension)
{
@ -219,6 +245,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
flags |= 16;
}
if (iccProfileBytes != null)
{
// Set iccp flag.
flags |= 32;
}
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
stream.Write(WebpConstants.Vp8XMagicBytes);
BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize);

26
src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
@ -406,6 +407,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
@ -415,6 +417,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
Stream stream,
ExifProfile exifProfile,
XmpProfile xmpProfile,
IccProfile iccProfile,
uint width,
uint height,
bool hasAlpha,
@ -424,6 +427,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
bool isVp8X = false;
byte[] exifBytes = null;
byte[] xmpBytes = null;
byte[] iccProfileBytes = null;
uint riffSize = 0;
if (exifProfile != null)
{
@ -439,6 +443,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
riffSize += this.MetadataChunkSize(xmpBytes);
}
if (iccProfile != null)
{
isVp8X = true;
iccProfileBytes = iccProfile.ToByteArray();
riffSize += this.MetadataChunkSize(iccProfileBytes);
}
if (hasAlpha)
{
isVp8X = true;
@ -457,7 +468,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
var bitWriterPartZero = new Vp8BitWriter(expectedSize);
// Partition #0 with header and partition sizes
// Partition #0 with header and partition sizes.
uint size0 = this.GeneratePartition0(bitWriterPartZero);
uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0;
@ -465,12 +476,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
uint pad = vp8Size & 1;
vp8Size += pad;
// Compute RIFF size
// Compute RIFF size.
// At the minimum it is: "WEBPVP8 nnnn" + VP8 data size.
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
// Emit headers and partition #0
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha, alphaData, alphaDataIsCompressed);
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed);
bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream.
@ -668,6 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
uint height,
ExifProfile exifProfile,
XmpProfile xmpProfile,
byte[] iccProfileBytes,
bool hasAlpha,
Span<byte> alphaData,
bool alphaDataIsCompressed)
@ -677,7 +689,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
// Write VP8X, header if necessary.
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha);
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfileBytes, width, height, hasAlpha);
if (iccProfileBytes != null)
{
this.WriteColorProfile(stream, iccProfileBytes);
}
if (hasAlpha)
{
this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed);

26
src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
@ -134,19 +135,20 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, IccProfile iccProfile, uint width, uint height, bool hasAlpha)
{
bool isVp8X = false;
byte[] exifBytes = null;
byte[] xmpBytes = null;
byte[] iccBytes = null;
uint riffSize = 0;
if (exifProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray();
riffSize += this.MetadataChunkSize(exifBytes);
}
@ -154,11 +156,22 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
if (xmpProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
xmpBytes = xmpProfile.Data;
riffSize += this.MetadataChunkSize(xmpBytes);
}
if (iccProfile != null)
{
isVp8X = true;
iccBytes = iccProfile.ToByteArray();
riffSize += this.MetadataChunkSize(iccBytes);
}
if (isVp8X)
{
riffSize += ExtendedFileChunkSize;
}
this.Finish();
uint size = (uint)this.NumBytes();
size++; // One byte extra for the VP8L signature.
@ -171,7 +184,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
// Write VP8X, header if necessary.
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha);
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha);
if (iccBytes != null)
{
this.WriteColorProfile(stream, iccBytes);
}
}
// Write magic bytes indicating its a lossless webp.

7
src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Webp
{
/// <summary>
@ -12,5 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
bool IgnoreMetadata { get; }
/// <summary>
/// Gets the decoding mode for multi-frame images.
/// </summary>
FrameDecodingMode DecodingMode { get; }
}
}

2
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -255,7 +255,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
this.EncodeStream(image);
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha);
this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha);
}
/// <summary>

2
src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs

@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
private static ReadOnlySpan<byte> LiteralMap => new byte[] { 0, 1, 1, 1, 0 };
/// <summary>
/// Decodes the image from the stream using the bitreader.
/// Decodes the lossless webp image from the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel buffer to store the decoded data.</param>

1
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -378,6 +378,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
stream,
metadata.ExifProfile,
metadata.XmpProfile,
metadata.IccProfile,
(uint)width,
(uint)height,
hasAlpha,

13
src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs

@ -57,7 +57,16 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
this.configuration = configuration;
}
public void Decode<TPixel>(Buffer2D<TPixel> pixels, int width, int height, WebpImageInfo info)
/// <summary>
/// Decodes the lossless webp image from the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel buffer to store the decoded data.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="info">Information about the image.</param>
/// <param name="alphaData">The ALPH chunk data.</param>
public void Decode<TPixel>(Buffer2D<TPixel> pixels, int width, int height, WebpImageInfo info, IMemoryOwner<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel>
{
// Paragraph 9.2: color space and clamp type follow.
@ -105,7 +114,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
using (var alphaDecoder = new AlphaDecoder(
width,
height,
info.Features.AlphaData,
alphaData,
info.Features.AlphaChunkHeader,
this.memoryAllocator,
this.configuration))

7
src/ImageSharp/Formats/Webp/MetadataExtensions.cs

@ -17,5 +17,12 @@ namespace SixLabors.ImageSharp
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="WebpMetadata"/>.</returns>
public static WebpMetadata GetWebpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
/// <summary>
/// Gets the webp format specific metadata for the image frame.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="WebpFrameMetadata"/>.</returns>
public static WebpFrameMetadata GetWebpMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
}
}

386
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -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();
}
}

375
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -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
};
}
}

9
src/ImageSharp/Formats/Webp/WebpDecoder.cs

@ -4,6 +4,7 @@
using System.IO;
using System.Threading;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp
@ -18,13 +19,19 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// </summary>
public bool IgnoreMetadata { get; set; }
/// <summary>
/// Gets or sets the decoding mode for multi-frame images.
/// Defaults to All.
/// </summary>
public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All;
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
var decoder = new WebpDecoderCore(configuration, this);
using var decoder = new WebpDecoderCore(configuration, this);
try
{

456
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -2,10 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
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;
@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// <summary>
/// Performs the webp decoding operation.
/// </summary>
internal sealed class WebpDecoderCore : IImageDecoderInternals
internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{
/// <summary>
/// Reusable buffer.
@ -29,14 +29,14 @@ namespace SixLabors.ImageSharp.Formats.Webp
private readonly byte[] buffer = new byte[4];
/// <summary>
/// Used for allocating memory during processing operations.
/// Used for allocating memory during the decoding operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The stream to decode from.
/// </summary>
private Stream currentStream;
private BufferedReadStream currentStream;
/// <summary>
/// The webp specific metadata.
@ -56,10 +56,19 @@ namespace SixLabors.ImageSharp.Formats.Webp
public WebpDecoderCore(Configuration configuration, IWebpDecoderOptions options)
{
this.Configuration = configuration;
this.DecodingMode = options.DecodingMode;
this.memoryAllocator = configuration.MemoryAllocator;
this.IgnoreMetadata = options.IgnoreMetadata;
}
/// <inheritdoc/>
public Configuration Configuration { get; }
/// <summary>
/// Gets the decoding mode for multi-frame images.
/// </summary>
public FrameDecodingMode DecodingMode { get; }
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
@ -70,14 +79,16 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// </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);
/// <summary>
/// Gets or sets the alpha data, if an ALPH chunk is present.
/// </summary>
public IMemoryOwner<byte> AlphaData { get; set; }
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
@ -92,6 +103,12 @@ namespace SixLabors.ImageSharp.Formats.Webp
using (this.webImageInfo = this.ReadVp8Info())
{
if (this.webImageInfo.Features is { Animation: true })
{
using var animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, this.Configuration, this.DecodingMode);
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
}
if (this.webImageInfo.Features is { Animation: true })
{
WebpThrowHelper.ThrowNotSupportedException("Animations are not supported");
@ -107,7 +124,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
else
{
var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration);
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo);
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.AlphaData);
}
// There can be optional chunks after the image data, like EXIF and XMP.
@ -132,7 +149,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
this.currentStream = stream;
this.ReadImageHeader();
using (this.webImageInfo = this.ReadVp8Info())
using (this.webImageInfo = this.ReadVp8Info(true))
{
return new ImageInfo(new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata);
}
@ -150,7 +167,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
// 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();
uint fileSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
// Skip 'WEBP' from the header.
this.currentStream.Skip(4);
@ -161,310 +178,59 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// <summary>
/// Reads information present in the image header, about the image content and how to decode the image.
/// </summary>
/// <param name="ignoreAlpha">For identify, the alpha data should not be read.</param>
/// <returns>Information about the webp image.</returns>
private WebpImageInfo ReadVp8Info()
private WebpImageInfo ReadVp8Info(bool ignoreAlpha = false)
{
this.Metadata = new ImageMetadata();
this.webpMetadata = this.Metadata.GetFormatMetadata(WebpFormat.Instance);
WebpChunkType chunkType = this.ReadChunkType();
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(this.currentStream, this.buffer);
var features = new WebpFeatures();
switch (chunkType)
{
case WebpChunkType.Vp8:
return this.ReadVp8Header();
this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, this.currentStream, this.buffer, features);
case WebpChunkType.Vp8L:
return this.ReadVp8LHeader();
this.webpMetadata.FileFormat = WebpFileFormatType.Lossless;
return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, this.currentStream, this.buffer, features);
case WebpChunkType.Vp8X:
return this.ReadVp8XHeader();
WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(this.currentStream, this.buffer, features);
while (this.currentStream.Position < this.currentStream.Length)
{
chunkType = WebpChunkParsingUtils.ReadChunkType(this.currentStream, this.buffer);
if (chunkType == WebpChunkType.Vp8)
{
this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, this.currentStream, this.buffer, features);
}
else if (chunkType == WebpChunkType.Vp8L)
{
this.webpMetadata.FileFormat = WebpFileFormatType.Lossless;
webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, this.currentStream, this.buffer, features);
}
else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType))
{
bool isAnimationChunk = this.ParseOptionalExtendedChunks(chunkType, features, ignoreAlpha);
if (isAnimationChunk)
{
return webpInfos;
}
}
else
{
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
}
}
return webpInfos;
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.
int imageFeatures = this.currentStream.ReadByte();
if (imageFeatures == -1)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8X header doe not contain enough data");
}
// 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.
int bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8X header does not contain enough data");
}
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.
bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the width");
return
new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception.
}
this.buffer[3] = 0;
uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
// 3 bytes for the height.
bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the height");
}
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).
int bytesRead = this.currentStream.Read(this.buffer, 0, 4);
if (bytesRead != 4)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 data size");
}
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.
bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 frame tag");
}
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.
bytesRead = this.currentStream.Read(this.buffer, 0, 3);
if (bytesRead != 3)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes");
}
if (!this.buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
{
WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
}
bytesRead = this.currentStream.Read(this.buffer, 0, 4);
if (bytesRead != 4)
{
WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the image width and height");
}
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>
@ -472,7 +238,9 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// </summary>
/// <param name="chunkType">The chunk type.</param>
/// <param name="features">The webp image features.</param>
private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features)
/// <param name="ignoreAlpha">For identify, the alpha data should not be read.</param>
/// <returns>true, if its a alpha chunk.</returns>
private bool ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features, bool ignoreAlpha)
{
switch (chunkType)
{
@ -488,32 +256,23 @@ namespace SixLabors.ImageSharp.Formats.Webp
this.ReadXmpProfile();
break;
case WebpChunkType.Animation:
// TODO: Decoding animation is not implemented yet.
break;
case WebpChunkType.AnimationParameter:
this.ReadAnimationParameters(features);
return true;
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);
int bytesRead = this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize);
if (bytesRead != alphaDataSize)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha chunk");
}
this.ReadAlphaData(features, ignoreAlpha);
break;
default:
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
break;
}
return false;
}
/// <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.
/// Reads the optional metadata EXIF of XMP profiles, which can follow the image data.
/// </summary>
/// <param name="features">The webp features.</param>
private void ParseOptionalChunks(WebpFeatures features)
@ -622,6 +381,53 @@ namespace SixLabors.ImageSharp.Formats.Webp
}
}
/// <summary>
/// Reads the animation parameters chunk from the stream.
/// </summary>
/// <param name="features">The webp features.</param>
private void ReadAnimationParameters(WebpFeatures features)
{
features.Animation = true;
uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
byte blue = (byte)this.currentStream.ReadByte();
byte green = (byte)this.currentStream.ReadByte();
byte red = (byte)this.currentStream.ReadByte();
byte alpha = (byte)this.currentStream.ReadByte();
features.AnimationBackgroundColor = new Color(new Rgba32(red, green, blue, alpha));
int bytesRead = this.currentStream.Read(this.buffer, 0, 2);
if (bytesRead != 2)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the animation loop count");
}
features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(this.buffer);
}
/// <summary>
/// Reads the alpha data chunk data from the stream.
/// </summary>
/// <param name="features">The features.</param>
/// <param name="ignoreAlpha">if set to true, skips the chunk data.</param>
private void ReadAlphaData(WebpFeatures features, bool ignoreAlpha)
{
uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
if (ignoreAlpha)
{
this.currentStream.Skip((int)alphaChunkSize);
return;
}
features.AlphaChunkHeader = (byte)this.currentStream.ReadByte();
int alphaDataSize = (int)(alphaChunkSize - 1);
this.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize);
Span<byte> alphaData = this.AlphaData.GetSpan();
int bytesRead = this.currentStream.Read(alphaData, 0, alphaDataSize);
if (bytesRead != alphaDataSize)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha data from the stream");
}
}
/// <summary>
/// Identifies the chunk type from the chunk.
/// </summary>
@ -655,19 +461,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
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
};
/// <inheritdoc/>
public void Dispose() => this.AlphaData?.Dispose();
}
}

22
src/ImageSharp/Formats/Webp/WebpFeatures.cs

@ -1,15 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
namespace SixLabors.ImageSharp.Formats.Webp
{
/// <summary>
/// Image features of a VP8X image.
/// </summary>
internal class WebpFeatures : IDisposable
internal class WebpFeatures
{
/// <summary>
/// Gets or sets a value indicating whether this image has an ICC Profile.
@ -21,11 +18,6 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// </summary>
public bool Alpha { get; set; }
/// <summary>
/// Gets or sets the alpha data, if an ALPH chunk is present.
/// </summary>
public IMemoryOwner<byte> AlphaData { get; set; }
/// <summary>
/// Gets or sets the alpha chunk header.
/// </summary>
@ -46,7 +38,15 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// </summary>
public bool Animation { get; set; }
/// <inheritdoc/>
public void Dispose() => this.AlphaData?.Dispose();
/// <summary>
/// Gets or sets the animation loop count. 0 means infinitely.
/// </summary>
public ushort AnimationLoopCount { get; set; }
/// <summary>
/// Gets or sets default background color of the animation frame canvas.
/// This color MAY be used to fill the unused space on the canvas around the frames, as well as the transparent pixels of the first frame..
/// </summary>
public Color? AnimationBackgroundColor { get; set; }
}
}

11
src/ImageSharp/Formats/Webp/WebpFormat.cs

@ -6,9 +6,9 @@ using System.Collections.Generic;
namespace SixLabors.ImageSharp.Formats.Webp
{
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the Webp format
/// Registers the image encoders, decoders and mime type detectors for the Webp format.
/// </summary>
public sealed class WebpFormat : IImageFormat<WebpMetadata>
public sealed class WebpFormat : IImageFormat<WebpMetadata, WebpFrameMetadata>
{
private WebpFormat()
{
@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// <summary>
/// Gets the current instance.
/// </summary>
public static WebpFormat Instance { get; } = new WebpFormat();
public static WebpFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "Webp";
@ -32,6 +32,9 @@ namespace SixLabors.ImageSharp.Formats.Webp
public IEnumerable<string> FileExtensions => WebpConstants.FileExtensions;
/// <inheritdoc/>
public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata();
public WebpMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}
}

33
src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs

@ -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);
}
}

1
src/ImageSharp/Formats/Webp/WebpImageInfo.cs

@ -63,7 +63,6 @@ namespace SixLabors.ImageSharp.Formats.Webp
{
this.Vp8BitReader?.Dispose();
this.Vp8LBitReader?.Dispose();
this.Features?.AlphaData?.Dispose();
}
}
}

11
src/ImageSharp/Formats/Webp/WebpMetadata.cs

@ -19,13 +19,22 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// Initializes a new instance of the <see cref="WebpMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private WebpMetadata(WebpMetadata other) => this.FileFormat = other.FileFormat;
private WebpMetadata(WebpMetadata other)
{
this.FileFormat = other.FileFormat;
this.AnimationLoopCount = other.AnimationLoopCount;
}
/// <summary>
/// Gets or sets the webp file format used. Either lossless or lossy.
/// </summary>
public WebpFileFormatType? FileFormat { get; set; }
/// <summary>
/// Gets or sets the loop count. The number of times to loop the animation. 0 means infinitely.
/// </summary>
public ushort AnimationLoopCount { get; set; } = 1;
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new WebpMetadata(this);
}

23
src/ImageSharp/Memory/Buffer2DRegion{T}.cs

@ -141,6 +141,9 @@ namespace SixLabors.ImageSharp.Memory
return ref this.Buffer.DangerousGetRowSpan(y)[x];
}
/// <summary>
/// Clears the contents of this <see cref="Buffer2DRegion{T}"/>.
/// </summary>
internal void Clear()
{
// Optimization for when the size of the area is the same as the buffer size.
@ -156,5 +159,25 @@ namespace SixLabors.ImageSharp.Memory
row.Clear();
}
}
/// <summary>
/// Fills the elements of this <see cref="Buffer2DRegion{T}"/> with the specified value.
/// </summary>
/// <param name="value">The value to assign to each element of the region.</param>
internal void Fill(T value)
{
// Optimization for when the size of the area is the same as the buffer size.
if (this.IsFullBufferArea)
{
this.Buffer.FastMemoryGroup.Fill(value);
return;
}
for (int y = 0; y < this.Rectangle.Height; y++)
{
Span<T> row = this.DangerousGetRowSpan(y);
row.Fill(value);
}
}
}
}

11
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs

@ -7,6 +7,12 @@ namespace SixLabors.ImageSharp.Memory
{
internal static class MemoryGroupExtensions
{
/// <summary>
/// Fills the elements of this <see cref="IMemoryGroup{T}"/> with the specified value.
/// </summary>
/// <typeparam name="T">The type of element.</typeparam>
/// <param name="group">The group to fill.</param>
/// <param name="value">The value to assign to each element of the group.</param>
internal static void Fill<T>(this IMemoryGroup<T> group, T value)
where T : struct
{
@ -16,6 +22,11 @@ namespace SixLabors.ImageSharp.Memory
}
}
/// <summary>
/// Clears the contents of this <see cref="IMemoryGroup{T}"/>.
/// </summary>
/// <typeparam name="T">The type of element.</typeparam>
/// <param name="group">The group to clear.</param>
internal static void Clear<T>(this IMemoryGroup<T> group)
where T : struct
{

4
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Metadata
/// </summary>
public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
{
private readonly Dictionary<IImageFormat, IDeepCloneable> formatMetadata = new Dictionary<IImageFormat, IDeepCloneable>();
private readonly Dictionary<IImageFormat, IDeepCloneable> formatMetadata = new();
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrameMetadata"/> class.
@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Metadata
public IptcProfile IptcProfile { get; set; }
/// <inheritdoc/>
public ImageFrameMetadata DeepClone() => new ImageFrameMetadata(this);
public ImageFrameMetadata DeepClone() => new(this);
/// <summary>
/// Gets the metadata value associated with the specified key.

2
src/ImageSharp/Metadata/ImageMetadata.cs

@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Metadata
/// </summary>
public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch;
private readonly Dictionary<IImageFormat, IDeepCloneable> formatMetadata = new Dictionary<IImageFormat, IDeepCloneable>();
private readonly Dictionary<IImageFormat, IDeepCloneable> formatMetadata = new();
private double horizontalResolution;
private double verticalResolution;

28
src/ImageSharp/Primitives/Rectangle.cs

@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp
public Point Location
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Point(this.X, this.Y);
get => new(this.X, this.Y);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
@ -98,7 +98,7 @@ namespace SixLabors.ImageSharp
public Size Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Size(this.Width, this.Height);
get => new(this.Width, this.Height);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
@ -147,14 +147,14 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="rectangle">The rectangle.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator RectangleF(Rectangle rectangle) => new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
public static implicit operator RectangleF(Rectangle rectangle) => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
/// <summary>
/// Creates a <see cref="Vector4"/> with the coordinates of the specified <see cref="Rectangle"/>.
/// </summary>
/// <param name="rectangle">The rectangle.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Vector4(Rectangle rectangle) => new Vector4(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
public static implicit operator Vector4(Rectangle rectangle) => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
/// <summary>
/// Compares two <see cref="Rectangle"/> objects for equality.
@ -188,7 +188,7 @@ namespace SixLabors.ImageSharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// ReSharper disable once InconsistentNaming
public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new Rectangle(left, top, unchecked(right - left), unchecked(bottom - top));
public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new(left, top, unchecked(right - left), unchecked(bottom - top));
/// <summary>
/// Returns the center point of the given <see cref="Rectangle"/>.
@ -196,7 +196,7 @@ namespace SixLabors.ImageSharp
/// <param name="rectangle">The rectangle.</param>
/// <returns>The <see cref="Point"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point Center(Rectangle rectangle) => new Point(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
public static Point Center(Rectangle rectangle) => new(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
/// <summary>
/// Creates a rectangle that represents the intersection between <paramref name="a"/> and
@ -376,7 +376,7 @@ namespace SixLabors.ImageSharp
public void Inflate(Size size) => this.Inflate(size.Width, size.Height);
/// <summary>
/// Determines if the specfied point is contained within the rectangular region defined by
/// Determines if the specified point is contained within the rectangular region defined by
/// this <see cref="Rectangle"/>.
/// </summary>
/// <param name="x">The x-coordinate of the given point.</param>
@ -405,10 +405,10 @@ namespace SixLabors.ImageSharp
(this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom);
/// <summary>
/// Determines if the specfied <see cref="Rectangle"/> intersects the rectangular region defined by
/// Determines if the specified <see cref="Rectangle"/> intersects the rectangular region defined by
/// this <see cref="Rectangle"/>.
/// </summary>
/// <param name="rectangle">The other Rectange. </param>
/// <param name="rectangle">The other Rectangle. </param>
/// <returns>The <see cref="bool"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IntersectsWith(Rectangle rectangle) =>
@ -438,16 +438,10 @@ namespace SixLabors.ImageSharp
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.X, this.Y, this.Width, this.Height);
}
public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Width, this.Height);
/// <inheritdoc/>
public override string ToString()
{
return $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
}
public override string ToString() => $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
/// <inheritdoc/>
public override bool Equals(object obj) => obj is Rectangle other && this.Equals(other);

4
tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs

@ -29,10 +29,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
public static readonly string[] BitfieldsBmpFiles = BitFields;
private static BmpDecoder BmpDecoder => new BmpDecoder();
private static BmpDecoder BmpDecoder => new();
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
new()
{
{ Car, 3780, 3780, PixelResolutionUnit.PixelsPerMeter },
{ V5Header, 3780, 3780, PixelResolutionUnit.PixelsPerMeter },

27
tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

@ -301,6 +301,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
public void Encode_PreservesAlpha<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
[Theory]
[WithFile(IccProfile, PixelTypes.Rgba32)]
public void Encode_PreservesColorProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> input = provider.GetImage(new BmpDecoder()))
{
ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile;
byte[] expectedProfileBytes = expectedProfile.ToByteArray();
using (var memStream = new MemoryStream())
{
input.Save(memStream, new BmpEncoder());
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile;
byte[] actualProfileBytes = actualProfile.ToByteArray();
Assert.NotNull(actualProfile);
Assert.Equal(expectedProfileBytes, actualProfileBytes);
}
}
}
}
[Theory]
[WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)]
[WithFile(V5Header, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)]

16
tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs

@ -3,7 +3,7 @@
using System.IO;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
@ -47,5 +47,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
Assert.Equal(expectedInfoHeaderType, bitmapMetadata.InfoHeaderType);
}
}
[Theory]
[WithFile(IccProfile, PixelTypes.Rgba32)]
public void Decoder_CanReadColorProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
{
ImageSharp.Metadata.ImageMetadata metaData = image.Metadata;
Assert.NotNull(metaData);
Assert.NotNull(metaData.IccProfile);
Assert.Equal(16, metaData.IccProfile.Entries.Length);
}
}
}
}

2
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -131,7 +131,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
public void Decode_WithInvalidDimensions_DoesThrowException<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
System.Exception ex = Record.Exception(
Exception ex = Record.Exception(
() =>
{
using Image<TPixel> image = provider.GetImage(GifDecoder);

1
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs

@ -302,6 +302,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{
PngChunkType.Header,
PngChunkType.Gamma,
PngChunkType.EmbeddedColorProfile,
PngChunkType.Palette,
PngChunkType.InternationalText,
PngChunkType.Text,

29
tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs

@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public class PngMetadataTests
{
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
new()
{
{ TestImages.Png.Splash, 11810, 11810, PixelResolutionUnit.PixelsPerMeter },
{ TestImages.Png.Ratio1x4, 1, 4, PixelResolutionUnit.AspectRatio },
@ -222,6 +222,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[Theory]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
public void Encode_PreservesColorProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> input = provider.GetImage(new PngDecoder()))
{
ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile;
byte[] expectedProfileBytes = expectedProfile.ToByteArray();
using (var memStream = new MemoryStream())
{
input.Save(memStream, new PngEncoder());
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile;
byte[] actualProfileBytes = actualProfile.ToByteArray();
Assert.NotNull(actualProfile);
Assert.Equal(expectedProfileBytes, actualProfileBytes);
}
}
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)

57
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -3,8 +3,10 @@
using System.IO;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
using Xunit;
using static SixLabors.ImageSharp.Tests.TestImages.Webp;
@ -34,7 +36,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
[InlineData(Lossless.NoTransform2, 128, 128, 32)]
[InlineData(Lossy.Alpha1, 1000, 307, 32)]
[InlineData(Lossy.Alpha2, 1000, 307, 32)]
[InlineData(Lossy.Bike, 250, 195, 24)]
[InlineData(Lossy.BikeWithExif, 250, 195, 24)]
public void Identify_DetectsCorrectDimensionsAndBitDepth(
string imagePath,
int expectedWidth,
@ -53,7 +55,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
}
[Theory]
[WithFile(Lossy.Bike, PixelTypes.Rgba32)]
[WithFile(Lossy.BikeWithExif, PixelTypes.Rgba32)]
[WithFile(Lossy.NoFilter01, PixelTypes.Rgba32)]
[WithFile(Lossy.NoFilter02, PixelTypes.Rgba32)]
[WithFile(Lossy.NoFilter03, PixelTypes.Rgba32)]
@ -234,7 +236,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
// TODO: Reference decoder throws here MagickCorruptImageErrorException, webpinfo also indicates an error here, but decoding the image seems to work.
// [WithFile(Lossless.GreenTransform5, PixelTypes.Rgba32)]
public void WebpDecoder_CanDecode_Lossless_WithSubstractGreenTransform<TPixel>(
public void WebpDecoder_CanDecode_Lossless_WithSubtractGreenTransform<TPixel>(
TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -330,6 +332,55 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
}
}
[Theory]
[WithFile(Lossless.Animated, PixelTypes.Rgba32)]
public void Decode_AnimatedLossless_VerifyAllFrames<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(WebpDecoder))
{
WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata();
WebpFrameMetadata frameMetaData = image.Frames.RootFrame.Metadata.GetWebpMetadata();
image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
Assert.Equal(0, webpMetaData.AnimationLoopCount);
Assert.Equal(150U, frameMetaData.FrameDuration);
Assert.Equal(12, image.Frames.Count);
}
}
[Theory]
[WithFile(Lossy.Animated, PixelTypes.Rgba32)]
public void Decode_AnimatedLossy_VerifyAllFrames<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(WebpDecoder))
{
WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata();
WebpFrameMetadata frameMetaData = image.Frames.RootFrame.Metadata.GetWebpMetadata();
image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f));
Assert.Equal(0, webpMetaData.AnimationLoopCount);
Assert.Equal(150U, frameMetaData.FrameDuration);
Assert.Equal(12, image.Frames.Count);
}
}
[Theory]
[WithFile(Lossless.Animated, PixelTypes.Rgba32)]
public void Decode_AnimatedLossless_WithFrameDecodingModeFirst_OnlyDecodesOneFrame<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new WebpDecoder() { DecodingMode = FrameDecodingMode.First }))
{
Assert.Equal(1, image.Frames.Count);
}
}
[Theory]
[WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)]
[WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)]

2
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
[Theory]
[WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy.
[WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]
[WithFile(Lossy.Bike, PixelTypes.Rgba32, WebpFileFormatType.Lossy)]
[WithFile(Lossy.BikeWithExif, PixelTypes.Rgba32, WebpFileFormatType.Lossy)]
public void Encode_PreserveRatio<TPixel>(TestImageProvider<TPixel> provider, WebpFileFormatType expectedFormat)
where TPixel : unmanaged, IPixel<TPixel>
{

59
tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs

@ -18,11 +18,31 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false };
[Theory]
[WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, true)]
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)]
public void IgnoreMetadata_ControlsWhetherExifIsParsed_WithLossyImage<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata };
using Image<TPixel> image = provider.GetImage(decoder);
if (ignoreMetadata)
{
Assert.Null(image.Metadata.ExifProfile);
}
else
{
ExifProfile exifProfile = image.Metadata.ExifProfile;
Assert.NotNull(exifProfile);
Assert.NotEmpty(exifProfile.Values);
Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Software) && m.GetValue().Equals("GIMP 2.10.2"));
}
}
[Theory]
[WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, true)]
public void IgnoreMetadata_ControlsWhetherExifIsParsed<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
public void IgnoreMetadata_ControlsWhetherExifIsParsed_WithLosslessImage<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata };
@ -111,7 +131,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
}
[Theory]
[WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32)]
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32)]
public void EncodeLossyWebp_PreservesExif<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -152,6 +172,37 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count);
}
[Theory]
[WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]
[WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossy)]
public void Encode_PreservesColorProfile<TPixel>(TestImageProvider<TPixel> provider, WebpFileFormatType fileFormat)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> input = provider.GetImage(new WebpDecoder()))
{
ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile;
byte[] expectedProfileBytes = expectedProfile.ToByteArray();
using (var memStream = new MemoryStream())
{
input.Save(memStream, new WebpEncoder()
{
FileFormat = fileFormat
});
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile;
byte[] actualProfileBytes = actualProfile.ToByteArray();
Assert.NotNull(actualProfile);
Assert.Equal(expectedProfileBytes, actualProfileBytes);
}
}
}
}
[Theory]
[WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)]
public void WebpDecoder_IgnoresInvalidExifChunk<TPixel>(TestImageProvider<TPixel> provider)

6
tests/ImageSharp.Tests/MemoryAllocatorValidator.cs

@ -59,9 +59,9 @@ namespace SixLabors.ImageSharp.Tests
public void Validate(int expectedAllocationCount)
{
var count = this.TotalRemainingAllocated;
var pass = expectedAllocationCount == count;
Assert.True(pass, $"Expected a {expectedAllocationCount} undisposed buffers but found {count}");
int count = this.TotalRemainingAllocated;
bool pass = expectedAllocationCount == count;
Assert.True(pass, $"Expected {expectedAllocationCount} undisposed buffers but found {count}");
}
public void Dispose()

15
tests/ImageSharp.Tests/TestImages.cs

@ -379,6 +379,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Rgb24jpeg = "Bmp/rgb24jpeg.bmp";
public const string Rgb24png = "Bmp/rgb24png.bmp";
public const string Rgba32v4 = "Bmp/rgba32v4.bmp";
public const string IccProfile = "Bmp/BMP_v5_with_ICC_2.bmp";
// Bitmap images with compression type BITFIELDS.
public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp";
@ -561,16 +562,9 @@ namespace SixLabors.ImageSharp.Tests
// Test images for converting rgb data to yuv.
public const string Yuv = "Webp/yuv_test.png";
public static class Animated
{
public const string Animated1 = "Webp/animated-webp.webp";
public const string Animated2 = "Webp/animated2.webp";
public const string Animated3 = "Webp/animated3.webp";
public const string Animated4 = "Webp/animated_lossy.webp";
}
public static class Lossless
{
public const string Animated = "Webp/leo_animated_lossless.webp";
public const string Earth = "Webp/earth_lossless.webp";
public const string Alpha = "Webp/lossless_alpha_small.webp";
public const string WithExif = "Webp/exif_lossless.webp";
@ -647,10 +641,11 @@ namespace SixLabors.ImageSharp.Tests
public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp";
public const string WithIccp = "Webp/lossy_with_iccp.webp";
public const string WithXmp = "Webp/xmp_lossy.webp";
public const string BikeSmall = "Webp/bike_lossless_small.webp";
public const string BikeSmall = "Webp/bike_lossy_small.webp";
public const string Animated = "Webp/leo_animated_lossy.webp";
// Lossy images without macroblock filtering.
public const string Bike = "Webp/bike_lossy.webp";
public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp";
public const string NoFilter01 = "Webp/vp80-01-intra-1400.webp";
public const string NoFilter02 = "Webp/vp80-00-comprehensive-010.webp";
public const string NoFilter03 = "Webp/vp80-00-comprehensive-005.webp";

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d99914f1a4dc3e554b9dded9e547194685b1b9ecc5d816d9f329cef483c525d5
size 50298

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:391ed80dc5ba4a21bdc4ea4db9fde4c6dad8556d1b8f0bf198db3c2bb5dc50ad
size 49389

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84bf215392014c2d7dbeb495bd1717bc2da4566b285bc388ed7bc8e88ebb0e85
size 52686

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e7a47ba473440f699f337fb8886cd170c6610452b3145c068a0f18584541559
size 53244

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4e7572c91c73e63e74c795e16ce951fbbdba5a015921102844d7bdf0fb0b473
size 56046

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6681af3640adb85452f9c1fa0cb5dce04638b48d80994c20c40d11e07670f1de
size 62469

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8549aeb786fc12d4e947b3b5f862701fab8158576193a03877f4b891815077e0
size 61068

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:474a6bbf07604de5a412d1eed2d3ba6ce191a85b88464c5848a50bef42566de5
size 60411

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f296bbd4b5637d1583ea337e8b807b34613640e0eabfb5e13e4e6cefe8ae2527
size 58793

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b16c16f9663b5ba80fa2ef06503851009b15700ff257375bd41cdb362098a391
size 57157

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5c39781b77219a6e9c05233d2376dfde04bd0dbe39f63274168073abf7a0e4d
size 55424

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5133dc9a5f8f6d26d388f40fd1df3a262f489d80a0d1eed588f7662bef7523de
size 59950

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d99914f1a4dc3e554b9dded9e547194685b1b9ecc5d816d9f329cef483c525d5
size 50298

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:391ed80dc5ba4a21bdc4ea4db9fde4c6dad8556d1b8f0bf198db3c2bb5dc50ad
size 49389

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84bf215392014c2d7dbeb495bd1717bc2da4566b285bc388ed7bc8e88ebb0e85
size 52686

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e7a47ba473440f699f337fb8886cd170c6610452b3145c068a0f18584541559
size 53244

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4e7572c91c73e63e74c795e16ce951fbbdba5a015921102844d7bdf0fb0b473
size 56046

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6681af3640adb85452f9c1fa0cb5dce04638b48d80994c20c40d11e07670f1de
size 62469

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8549aeb786fc12d4e947b3b5f862701fab8158576193a03877f4b891815077e0
size 61068

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:474a6bbf07604de5a412d1eed2d3ba6ce191a85b88464c5848a50bef42566de5
size 60411

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f296bbd4b5637d1583ea337e8b807b34613640e0eabfb5e13e4e6cefe8ae2527
size 58793

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b16c16f9663b5ba80fa2ef06503851009b15700ff257375bd41cdb362098a391
size 57157

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5c39781b77219a6e9c05233d2376dfde04bd0dbe39f63274168073abf7a0e4d
size 55424

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5133dc9a5f8f6d26d388f40fd1df3a262f489d80a0d1eed588f7662bef7523de
size 59950

3
tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5b483e9a9d3f3ebdeada2eff70800002c27c046bf971206af0ecc73fa1416e6
size 27782

0
tests/Images/Input/Webp/bike_lossless_small.webp → tests/Images/Input/Webp/bike_lossy_small.webp

0
tests/Images/Input/Webp/bike_lossy.webp → tests/Images/Input/Webp/bike_lossy_with_exif.webp

3
tests/Images/Input/Webp/leo_animated_lossless.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bab815db08e8f413c7a355b7e9c152e1a73e503392012af16ada92858706d255
size 400342

3
tests/Images/Input/Webp/leo_animated_lossy.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00fffbb0d67b0336574d9bad9cbacaf97d81f2e70db3d458508c430e3d103228
size 64972
Loading…
Cancel
Save