diff --git a/src/ImageSharp/Formats/Bmp/BmpColorSpace.cs b/src/ImageSharp/Formats/Bmp/BmpColorSpace.cs
new file mode 100644
index 0000000000..8640871219
--- /dev/null
+++ b/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
+{
+ ///
+ /// Enum for the different color spaces.
+ ///
+ internal enum BmpColorSpace
+ {
+ ///
+ /// This value implies that endpoints and gamma values are given in the appropriate fields.
+ ///
+ LCS_CALIBRATED_RGB = 0,
+
+ ///
+ /// The Windows default color space ('Win ').
+ ///
+ LCS_WINDOWS_COLOR_SPACE = 1466527264,
+
+ ///
+ /// Specifies that the bitmap is in sRGB color space ('sRGB').
+ ///
+ LCS_sRGB = 1934772034,
+
+ ///
+ /// This value indicates that bV5ProfileData points to the file name of the profile to use (gamma and endpoints values are ignored).
+ ///
+ PROFILE_LINKED = 1279872587,
+
+ ///
+ /// This value indicates that bV5ProfileData points to a memory buffer that contains the profile to be used (gamma and endpoints values are ignored).
+ ///
+ PROFILE_EMBEDDED = 1296188740
+ }
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index a22a04980c..26687ff16f 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/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 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)
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 6384074df3..247ed78117 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/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
///
/// 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.
///
- private readonly bool writeV4Header;
+ private BmpInfoHeaderType infoHeaderType;
///
/// 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;
}
///
@@ -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 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();
+ }
+
+ ///
+ /// Creates the bitmap information header.
+ ///
+ /// The width of the image.
+ /// The height of the image.
+ /// Size of the information header.
+ /// The bits per pixel.
+ /// The bytes per line.
+ /// The metadata.
+ /// The icc profile data.
+ /// The bitmap information header.
+ 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;
+ }
+
+ ///
+ /// Writes the color profile to the stream.
+ ///
+ /// The stream to write to.
+ /// The color profile data.
+ /// The buffer.
+ private void WriteColorProfile(Stream stream, byte[] iccProfileData, Span 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));
}
+ }
+ ///
+ /// Writes the bitmap file header.
+ ///
+ /// The stream to write the header to.
+ /// Size of the bitmap information header.
+ /// Size of the color palette.
+ /// The size in bytes of the color profile.
+ /// The information header to write.
+ /// The buffer to write to.
+ private void WriteBitmapFileHeader(Stream stream, int infoHeaderSize, int colorPaletteSize, int iccProfileSize, BmpInfoHeader infoHeader, Span 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 buffer = stackalloc byte[infoHeaderSize];
fileHeader.WriteTo(buffer);
-
stream.Write(buffer, 0, BmpFileHeader.Size);
+ }
- if (this.writeV4Header)
- {
- infoHeader.WriteV4Header(buffer);
- }
- else
+ ///
+ /// Writes the bitmap information header.
+ ///
+ /// The stream to write info header into.
+ /// The information header.
+ /// The buffer.
+ /// Size of the information header.
+ private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span 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();
}
///
diff --git a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
index acbcdaef3a..ab56bd246b 100644
--- a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
@@ -57,10 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
public int Offset { get; }
- public static BmpFileHeader Parse(Span data)
- {
- return MemoryMarshal.Cast(data)[0];
- }
+ public static BmpFileHeader Parse(Span data) => MemoryMarshal.Cast(data)[0];
public void WriteTo(Span buffer)
{
diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
index 0d0c05c9f4..31394821f8 100644
--- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
+++ b/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;
}
///
@@ -211,7 +219,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Gets or sets the Color space type. Not used yet.
///
- public int CsType { get; set; }
+ public BmpColorSpace CsType { get; set; }
///
/// Gets or sets the X coordinate of red endpoint. Not used yet.
@@ -273,21 +281,38 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
public int GammaBlue { get; set; }
+ ///
+ /// Gets or sets the rendering intent for bitmap.
+ ///
+ public BmpRenderingIntent Intent { get; set; }
+
+ ///
+ /// Gets or sets the offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data.
+ ///
+ public int ProfileData { get; set; }
+
+ ///
+ /// Gets or sets the size, in bytes, of embedded profile data.
+ ///
+ public int ProfileSize { get; set; }
+
+ ///
+ /// Gets or sets the reserved value.
+ ///
+ public int Reserved { get; set; }
+
///
/// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
///
/// The data to parse.
/// The parsed header.
///
- public static BmpInfoHeader ParseCore(ReadOnlySpan data)
- {
- return new BmpInfoHeader(
+ public static BmpInfoHeader ParseCore(ReadOnlySpan 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)));
- }
///
/// 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
/// The data to parse.
/// The parsed header.
///
- public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data)
- {
- return new BmpInfoHeader(
+ public static BmpInfoHeader ParseOs22Short(ReadOnlySpan 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)));
- }
///
/// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes).
@@ -312,9 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// The data to parse.
/// The parsed header.
///
- public static BmpInfoHeader ParseV3(ReadOnlySpan data)
- {
- return new BmpInfoHeader(
+ public static BmpInfoHeader ParseV3(ReadOnlySpan 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)));
- }
///
/// 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
/// Indicates, if the alpha bitmask is present.
/// The parsed header.
///
- public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true)
- {
- return new BmpInfoHeader(
+ public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan 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);
- }
///
/// 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
/// The data to parse.
/// The parsed header.
///
- public static BmpInfoHeader ParseV4(ReadOnlySpan data)
+ public static BmpInfoHeader ParseV4(ReadOnlySpan 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)));
+
+ ///
+ /// Parses the full BMP Version 5 BITMAPINFOHEADER header (124 bytes).
+ ///
+ /// The data to parse.
+ /// The parsed header.
+ ///
+ public static BmpInfoHeader ParseV5(ReadOnlySpan 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(data)[0];
@@ -448,6 +500,43 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// The buffer to write to.
public void WriteV4Header(Span 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);
+ }
+
+ ///
+ /// Writes a complete Bitmap V5 header to a buffer.
+ ///
+ /// The buffer to write to.
+ public void WriteV5Header(Span buffer)
{
ref BmpInfoHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer));
diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs
new file mode 100644
index 0000000000..e437a0cbf8
--- /dev/null
+++ b/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
+{
+ ///
+ /// Enum for the different rendering intent's.
+ ///
+ internal enum BmpRenderingIntent
+ {
+ ///
+ /// Invalid default value.
+ ///
+ Invalid = 0,
+
+ ///
+ /// Maintains saturation. Used for business charts and other situations in which undithered colors are required.
+ ///
+ LCS_GM_BUSINESS = 1,
+
+ ///
+ /// Maintains colorimetric match. Used for graphic designs and named colors.
+ ///
+ LCS_GM_GRAPHICS = 2,
+
+ ///
+ /// Maintains contrast. Used for photographs and natural images.
+ ///
+ LCS_GM_IMAGES = 4,
+
+ ///
+ /// Maintains the white point. Matches the colors to their nearest color in the destination gamut.
+ ///
+ LCS_GM_ABS_COLORIMETRIC = 8,
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 12770bc521..f46b5058a1 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/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;
}
+ ///
+ /// Reads the color profile chunk. The data is stored similar to the zTXt chunk.
+ ///
+ /// The metadata.
+ /// The bytes containing the profile.
+ private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan 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 keywordBytes = data.Slice(0, zeroIndex);
+ if (!this.TryReadTextKeyword(keywordBytes, out string name))
+ {
+ return;
+ }
+
+ ReadOnlySpan compressedData = data.Slice(zeroIndex + 2);
+
+ if (this.TryUncompressZlibData(compressedData, out byte[] iccpProfileBytes))
+ {
+ metadata.IccProfile = new IccProfile(iccpProfileBytes);
+ }
+ }
+
+ ///
+ /// Tries to un-compress zlib compressed data.
+ ///
+ /// The compressed data.
+ /// The uncompressed bytes array.
+ /// True, if de-compressing was successful.
+ private unsafe bool TryUncompressZlibData(ReadOnlySpan compressedData, out byte[] uncompressedBytesArray)
+ {
+ fixed (byte* compressedDataBase = compressedData)
+ {
+ using (IMemoryOwner destBuffer = this.memoryAllocator.Allocate(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 destUncompressedData = destBuffer.GetSpan();
+ if (!inflateStream.AllocateNewBytes(compressedData.Length, false))
+ {
+ uncompressedBytesArray = Array.Empty();
+ 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;
+ }
+ }
+ }
+
///
/// Compares two ReadOnlySpan<char>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
/// The .
private bool TryUncompressTextData(ReadOnlySpan 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();
-
- // 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;
}
///
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index c443c0fcf1..ad16c80374 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -87,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.Png
///
private IMemoryOwner currentScanline;
+ ///
+ /// The color profile name.
+ ///
+ private const string ColorProfileName = "ICC Profile";
+
///
/// Initializes a new instance of the class.
///
@@ -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
}
///
- /// 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.
///
/// The containing image data.
/// The image metadata.
@@ -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 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);
}
}
+ ///
+ /// Writes the color profile chunk.
+ ///
+ /// The stream to write to.
+ /// The image meta data.
+ 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 owner = this.memoryAllocator.Allocate(payloadLength))
+ {
+ Span 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);
+ }
+ }
+
///
/// 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 owner = this.memoryAllocator.Allocate(payloadLength))
{
Span 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
///
/// Compresses a given text using Zlib compression.
///
- /// The text bytes to compress.
- /// The compressed text byte array.
- private byte[] GetCompressedTextBytes(byte[] textBytes)
+ /// The bytes to compress.
+ /// The compressed byte array.
+ 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();
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index fc1accfdee..0e1c9f4d95 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/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
}
///
- /// Calculates the chunk size of EXIF or XMP metadata.
+ /// Calculates the chunk size of EXIF, XMP or ICCP metadata.
///
/// The metadata profile bytes.
/// The metadata chunk size in bytes.
@@ -178,16 +179,41 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
}
}
+ ///
+ /// Writes the color profile to the stream.
+ ///
+ /// The stream to write to.
+ /// The color profile bytes.
+ protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes)
+ {
+ uint size = (uint)iccProfileBytes.Length;
+
+ Span 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);
+ }
+ }
+
///
/// Writes a VP8X header to the stream.
///
/// The stream to write to.
/// A exif profile or null, if it does not exist.
/// A XMP profile or null, if it does not exist.
+ /// The color profile bytes.
/// The width of the image.
/// The height of the image.
/// Flag indicating, if a alpha channel is present.
- 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 buf = this.scratchBuffer.AsSpan(0, 4);
stream.Write(WebpConstants.Vp8XMagicBytes);
BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize);
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
index fa6e09d875..7cc915e18f 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
+++ b/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
/// The stream to write to.
/// The exif profile.
/// The XMP profile.
+ /// The color profile.
/// The width of the image.
/// The height of the image.
/// Flag indicating, if a alpha channel is present.
@@ -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 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);
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
index d41224f908..7bd6febeb6 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
+++ b/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
/// The stream to write to.
/// The exif profile.
/// The XMP profile.
+ /// The color profile.
/// The width of the image.
/// The height of the image.
/// Flag indicating, if a alpha channel is present.
- 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.
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 30d65562ae..8de1ccc420 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/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);
}
///
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index 695359e5ea..f24fdb45d8 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/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,
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index 43ec45a34f..b42569a232 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/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 RatioFiles =
- new TheoryData
+ new()
{
{ Car, 3780, 3780, PixelResolutionUnit.PixelsPerMeter },
{ V5Header, 3780, 3780, PixelResolutionUnit.PixelsPerMeter },
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
index 073cf5fcf2..2215d5f0a5 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
@@ -301,6 +301,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
public void Encode_PreservesAlpha(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
+ [Theory]
+ [WithFile(IccProfile, PixelTypes.Rgba32)]
+ public void Encode_PreservesColorProfile(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(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)]
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs
index 8931c242ef..6a582e0199 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs
+++ b/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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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);
+ }
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
index 30a6847029..ede4ec1ccc 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
+++ b/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,
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
index fd39a828f9..d0ae61fd4c 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
@@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public class PngMetadataTests
{
public static readonly TheoryData RatioFiles =
- new TheoryData
+ 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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(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)
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
index 35d285d0d6..1787afb93e 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
+++ b/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);
}
+ [Theory]
+ [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]
+ [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossy)]
+ public void Encode_PreservesColorProfile(TestImageProvider provider, WebpFileFormatType fileFormat)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image 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(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(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 96da6c608c..a3ef6942f3 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/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";
diff --git a/tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp b/tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp
new file mode 100644
index 0000000000..d6328d429f
--- /dev/null
+++ b/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