Browse Source

Merge branch 'main' into bp/webpanimation

pull/1985/head
Brian Popow 4 years ago
committed by GitHub
parent
commit
4dd0dc9511
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. 101
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  8. 81
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  9. 36
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  10. 26
      src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
  11. 26
      src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
  12. 2
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  13. 1
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  14. 4
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  15. 27
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  16. 16
      tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs
  17. 1
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
  18. 29
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  19. 31
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  20. 1
      tests/ImageSharp.Tests/TestImages.cs
  21. 3
      tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp

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

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

@ -3,6 +3,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Buffers.Binary;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
@ -79,9 +80,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary> /// <summary>
/// A bitmap v4 header will only be written, if the user explicitly wants support for transparency. /// 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. /// 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. /// Otherwise a bitmap v3 header will be written, which is supported by almost all decoders.
/// </summary> /// </summary>
private readonly bool writeV4Header; private BmpInfoHeaderType infoHeaderType;
/// <summary> /// <summary>
/// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images. /// 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.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel; this.bitsPerPixel = options.BitsPerPixel;
this.writeV4Header = options.SupportTransparency;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
} }
/// <summary> /// <summary>
@ -123,7 +125,62 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32);
this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); 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 hResolution = 0;
int vResolution = 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( var infoHeader = new BmpInfoHeader(
headerSize: infoHeaderSize, headerSize: infoHeaderSize,
height: image.Height, height: height,
width: image.Width, width: width,
bitsPerPixel: bpp, bitsPerPixel: bpp,
planes: 1, planes: 1,
imageSize: image.Height * bytesPerLine, imageSize: height * bytesPerLine,
clrUsed: 0, clrUsed: 0,
clrImportant: 0, clrImportant: 0,
xPelsPerMeter: hResolution, xPelsPerMeter: hResolution,
yPelsPerMeter: vResolution); 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.AlphaMask = Rgba32AlphaMask;
infoHeader.RedMask = Rgba32RedMask; infoHeader.RedMask = Rgba32RedMask;
@ -176,45 +232,79 @@ namespace SixLabors.ImageSharp.Formats.Bmp
infoHeader.Compression = BmpCompression.BitFields; infoHeader.Compression = BmpCompression.BitFields;
} }
int colorPaletteSize = 0; if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && metadata.IccProfile != null)
if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8)
{ {
colorPaletteSize = ColorPaletteSize8Bit; infoHeader.ProfileSize = iccProfileData.Length;
infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED;
infoHeader.Intent = BmpRenderingIntent.LCS_GM_IMAGES;
} }
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4)
{ return infoHeader;
colorPaletteSize = ColorPaletteSize4Bit; }
}
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1) /// <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( var fileHeader = new BmpFileHeader(
type: BmpConstants.TypeMarkers.Bitmap, type: BmpConstants.TypeMarkers.Bitmap,
fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + infoHeader.ImageSize, fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + iccProfileSize + infoHeader.ImageSize,
reserved: 0, reserved: 0,
offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize); offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize);
Span<byte> buffer = stackalloc byte[infoHeaderSize];
fileHeader.WriteTo(buffer); fileHeader.WriteTo(buffer);
stream.Write(buffer, 0, BmpFileHeader.Size); stream.Write(buffer, 0, BmpFileHeader.Size);
}
if (this.writeV4Header) /// <summary>
{ /// Writes the bitmap information header.
infoHeader.WriteV4Header(buffer); /// </summary>
} /// <param name="stream">The stream to write info header into.</param>
else /// <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); stream.Write(buffer, 0, infoHeaderSize);
this.WriteImage(stream, image.Frames.RootFrame);
stream.Flush();
} }
/// <summary> /// <summary>

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

@ -57,10 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary> /// </summary>
public int Offset { get; } public int Offset { get; }
public static BmpFileHeader Parse(Span<byte> data) public static BmpFileHeader Parse(Span<byte> data) => MemoryMarshal.Cast<byte, BmpFileHeader>(data)[0];
{
return MemoryMarshal.Cast<byte, BmpFileHeader>(data)[0];
}
public void WriteTo(Span<byte> buffer) 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 greenMask = 0,
int blueMask = 0, int blueMask = 0,
int alphaMask = 0, int alphaMask = 0,
int csType = 0, BmpColorSpace csType = 0,
int redX = 0, int redX = 0,
int redY = 0, int redY = 0,
int redZ = 0, int redZ = 0,
@ -94,7 +94,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int blueZ = 0, int blueZ = 0,
int gammeRed = 0, int gammeRed = 0,
int gammeGreen = 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.HeaderSize = headerSize;
this.Width = width; this.Width = width;
@ -124,6 +128,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.GammaRed = gammeRed; this.GammaRed = gammeRed;
this.GammaGreen = gammeGreen; this.GammaGreen = gammeGreen;
this.GammaBlue = gammeBlue; this.GammaBlue = gammeBlue;
this.Intent = intent;
this.ProfileData = profileData;
this.ProfileSize = profileSize;
this.Reserved = reserved;
} }
/// <summary> /// <summary>
@ -211,7 +219,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary> /// <summary>
/// Gets or sets the Color space type. Not used yet. /// Gets or sets the Color space type. Not used yet.
/// </summary> /// </summary>
public int CsType { get; set; } public BmpColorSpace CsType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the X coordinate of red endpoint. Not used yet. /// Gets or sets the X coordinate of red endpoint. Not used yet.
@ -273,21 +281,38 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary> /// </summary>
public int GammaBlue { get; set; } 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> /// <summary>
/// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). /// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
/// </summary> /// </summary>
/// <param name="data">The data to parse.</param> /// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns> /// <returns>The parsed header.</returns>
/// <seealso href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183372.aspx"/> /// <seealso href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183372.aspx"/>
public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data) public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data) => new(
{
return new BmpInfoHeader(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(4, 2)), width: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(4, 2)),
height: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(6, 2)), height: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(6, 2)),
planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(8, 2)), planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(8, 2)),
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(10, 2))); bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(10, 2)));
}
/// <summary> /// <summary>
/// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height /// 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> /// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns> /// <returns>The parsed header.</returns>
/// <seealso href="https://www.fileformat.info/format/os2bmp/egff.htm"/> /// <seealso href="https://www.fileformat.info/format/os2bmp/egff.htm"/>
public static BmpInfoHeader ParseOs22Short(ReadOnlySpan<byte> data) public static BmpInfoHeader ParseOs22Short(ReadOnlySpan<byte> data) => new(
{
return new BmpInfoHeader(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)),
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2))); bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)));
}
/// <summary> /// <summary>
/// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes). /// 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> /// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns> /// <returns>The parsed header.</returns>
/// <seealso href="http://www.fileformat.info/format/bmp/egff.htm"/> /// <seealso href="http://www.fileformat.info/format/bmp/egff.htm"/>
public static BmpInfoHeader ParseV3(ReadOnlySpan<byte> data) public static BmpInfoHeader ParseV3(ReadOnlySpan<byte> data) => new(
{
return new BmpInfoHeader(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
@ -326,7 +346,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)),
clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)),
clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4))); clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)));
}
/// <summary> /// <summary>
/// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. /// 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> /// <param name="withAlpha">Indicates, if the alpha bitmask is present.</param>
/// <returns>The parsed header.</returns> /// <returns>The parsed header.</returns>
/// <seealso href="https://forums.adobe.com/message/3272950#3272950"/> /// <seealso href="https://forums.adobe.com/message/3272950#3272950"/>
public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan<byte> data, bool withAlpha = true) public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan<byte> data, bool withAlpha = true) => new(
{
return new BmpInfoHeader(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
@ -354,7 +371,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)),
blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)),
alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0); alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0);
}
/// <summary> /// <summary>
/// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are /// 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> /// <param name="data">The data to parse.</param>
/// <returns>The parsed header.</returns> /// <returns>The parsed header.</returns>
/// <seealso href="http://www.fileformat.info/format/bmp/egff.htm"/> /// <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]; return MemoryMarshal.Cast<byte, BmpInfoHeader>(data)[0];
@ -448,6 +500,43 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary> /// </summary>
/// <param name="buffer">The buffer to write to.</param> /// <param name="buffer">The buffer to write to.</param>
public void WriteV4Header(Span<byte> buffer) 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)); 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,
}
}

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

@ -19,6 +19,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -205,6 +206,9 @@ namespace SixLabors.ImageSharp.Formats.Png
this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true);
} }
break;
case PngChunkType.EmbeddedColorProfile:
this.ReadColorProfileChunk(metadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.End: case PngChunkType.End:
goto EOF; goto EOF;
@ -1174,6 +1178,76 @@ namespace SixLabors.ImageSharp.Formats.Png
return true; 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> /// <summary>
/// Compares two ReadOnlySpan&lt;char&gt;s in a case-insensitive method. /// Compares two ReadOnlySpan&lt;char&gt;s in a case-insensitive method.
/// This is only needed because older frameworks are missing the extension 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)) else if (this.IsXmpTextData(keywordBytes))
{ {
XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray()); var xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray());
metadata.XmpProfile = xmpProfile; metadata.XmpProfile = xmpProfile;
} }
else else
@ -1325,29 +1399,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <returns>The <see cref="bool"/>.</returns> /// <returns>The <see cref="bool"/>.</returns>
private bool TryUncompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding, out string value) private bool TryUncompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding, out string value)
{ {
using (var memoryStream = new MemoryStream(compressedData.ToArray())) if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData))
using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream))
using (var inflateStream = new ZlibInflateStream(bufferedStream))
{ {
if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) value = encoding.GetString(uncompressedData);
{
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());
return true; return true;
} }
value = null;
return false;
} }
/// <summary> /// <summary>

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

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

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

@ -5,6 +5,7 @@ using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
@ -97,7 +98,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
} }
/// <summary> /// <summary>
/// Calculates the chunk size of EXIF or XMP metadata. /// Calculates the chunk size of EXIF, XMP or ICCP metadata.
/// </summary> /// </summary>
/// <param name="metadataBytes">The metadata profile bytes.</param> /// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The metadata chunk size in bytes.</returns> /// <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> /// <summary>
/// Writes a VP8X header to the stream. /// Writes a VP8X header to the stream.
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <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="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="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="width">The width of the image.</param>
/// <param name="height">The height 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> /// <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) if (width > MaxDimension || height > MaxDimension)
{ {
@ -219,6 +245,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
flags |= 16; flags |= 16;
} }
if (iccProfileBytes != null)
{
// Set iccp flag.
flags |= 32;
}
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4); Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
stream.Write(WebpConstants.Vp8XMagicBytes); stream.Write(WebpConstants.Vp8XMagicBytes);
BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); 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 System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter 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="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param> /// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP 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="width">The width of the image.</param>
/// <param name="height">The height 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> /// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
@ -415,6 +417,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
Stream stream, Stream stream,
ExifProfile exifProfile, ExifProfile exifProfile,
XmpProfile xmpProfile, XmpProfile xmpProfile,
IccProfile iccProfile,
uint width, uint width,
uint height, uint height,
bool hasAlpha, bool hasAlpha,
@ -424,6 +427,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
bool isVp8X = false; bool isVp8X = false;
byte[] exifBytes = null; byte[] exifBytes = null;
byte[] xmpBytes = null; byte[] xmpBytes = null;
byte[] iccProfileBytes = null;
uint riffSize = 0; uint riffSize = 0;
if (exifProfile != null) if (exifProfile != null)
{ {
@ -439,6 +443,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
riffSize += this.MetadataChunkSize(xmpBytes); riffSize += this.MetadataChunkSize(xmpBytes);
} }
if (iccProfile != null)
{
isVp8X = true;
iccProfileBytes = iccProfile.ToByteArray();
riffSize += this.MetadataChunkSize(iccProfileBytes);
}
if (hasAlpha) if (hasAlpha)
{ {
isVp8X = true; isVp8X = true;
@ -457,7 +468,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
var bitWriterPartZero = new Vp8BitWriter(expectedSize); 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 size0 = this.GeneratePartition0(bitWriterPartZero);
uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0;
@ -465,12 +476,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
uint pad = vp8Size & 1; uint pad = vp8Size & 1;
vp8Size += pad; vp8Size += pad;
// Compute RIFF size // Compute RIFF size.
// At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size.
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
// Emit headers and partition #0 // 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); bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream. // Write the encoded image to the stream.
@ -668,6 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
uint height, uint height,
ExifProfile exifProfile, ExifProfile exifProfile,
XmpProfile xmpProfile, XmpProfile xmpProfile,
byte[] iccProfileBytes,
bool hasAlpha, bool hasAlpha,
Span<byte> alphaData, Span<byte> alphaData,
bool alphaDataIsCompressed) bool alphaDataIsCompressed)
@ -677,7 +689,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
// Write VP8X, header if necessary. // Write VP8X, header if necessary.
if (isVp8X) 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) if (hasAlpha)
{ {
this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); 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 System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter 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="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param> /// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP 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="width">The width of the image.</param>
/// <param name="height">The height 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> /// <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; bool isVp8X = false;
byte[] exifBytes = null; byte[] exifBytes = null;
byte[] xmpBytes = null; byte[] xmpBytes = null;
byte[] iccBytes = null;
uint riffSize = 0; uint riffSize = 0;
if (exifProfile != null) if (exifProfile != null)
{ {
isVp8X = true; isVp8X = true;
riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray(); exifBytes = exifProfile.ToByteArray();
riffSize += this.MetadataChunkSize(exifBytes); riffSize += this.MetadataChunkSize(exifBytes);
} }
@ -154,11 +156,22 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
if (xmpProfile != null) if (xmpProfile != null)
{ {
isVp8X = true; isVp8X = true;
riffSize += ExtendedFileChunkSize;
xmpBytes = xmpProfile.Data; xmpBytes = xmpProfile.Data;
riffSize += this.MetadataChunkSize(xmpBytes); riffSize += this.MetadataChunkSize(xmpBytes);
} }
if (iccProfile != null)
{
isVp8X = true;
iccBytes = iccProfile.ToByteArray();
riffSize += this.MetadataChunkSize(iccBytes);
}
if (isVp8X)
{
riffSize += ExtendedFileChunkSize;
}
this.Finish(); this.Finish();
uint size = (uint)this.NumBytes(); uint size = (uint)this.NumBytes();
size++; // One byte extra for the VP8L signature. size++; // One byte extra for the VP8L signature.
@ -171,7 +184,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
// Write VP8X, header if necessary. // Write VP8X, header if necessary.
if (isVp8X) 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. // Write magic bytes indicating its a lossless webp.

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

@ -255,7 +255,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
this.EncodeStream(image); this.EncodeStream(image);
// Write bytes from the bitwriter buffer to the stream. // 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> /// <summary>

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

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

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

@ -29,10 +29,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
public static readonly string[] BitfieldsBmpFiles = BitFields; 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 = public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit> new()
{ {
{ Car, 3780, 3780, PixelResolutionUnit.PixelsPerMeter }, { Car, 3780, 3780, PixelResolutionUnit.PixelsPerMeter },
{ V5Header, 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) public void Encode_PreservesAlpha<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); 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] [Theory]
[WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] [WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)]
[WithFile(V5Header, 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 System.IO;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit; using Xunit;
using static SixLabors.ImageSharp.Tests.TestImages.Bmp; using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
@ -47,5 +47,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
Assert.Equal(expectedInfoHeaderType, bitmapMetadata.InfoHeaderType); 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);
}
}
} }
} }

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

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

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

@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public class PngMetadataTests public class PngMetadataTests
{ {
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles = 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.Splash, 11810, 11810, PixelResolutionUnit.PixelsPerMeter },
{ TestImages.Png.Ratio1x4, 1, 4, PixelResolutionUnit.AspectRatio }, { 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] [Theory]
[MemberData(nameof(RatioFiles))] [MemberData(nameof(RatioFiles))]
public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)

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

@ -172,6 +172,37 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); 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] [Theory]
[WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)] [WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)]
public void WebpDecoder_IgnoresInvalidExifChunk<TPixel>(TestImageProvider<TPixel> provider) public void WebpDecoder_IgnoresInvalidExifChunk<TPixel>(TestImageProvider<TPixel> provider)

1
tests/ImageSharp.Tests/TestImages.cs

@ -379,6 +379,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Rgb24jpeg = "Bmp/rgb24jpeg.bmp"; public const string Rgb24jpeg = "Bmp/rgb24jpeg.bmp";
public const string Rgb24png = "Bmp/rgb24png.bmp"; public const string Rgb24png = "Bmp/rgb24png.bmp";
public const string Rgba32v4 = "Bmp/rgba32v4.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. // Bitmap images with compression type BITFIELDS.
public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp"; public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp";

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
Loading…
Cancel
Save