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.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 @@ public Image Decode(BufferedReadStream stream, CancellationToken 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 @@ private static int CountBits(uint n) 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 @@ private void ReadInfoHeader() 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 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b 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 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b 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 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals /// /// 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 @@ public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocato { 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 @@ public void Encode(Image image, Stream stream, CancellationToken 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 @@ public void Encode(Image image, Stream stream, CancellationToken } } - 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 @@ public void Encode(Image image, Stream stream, CancellationToken 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 @@ public BmpFileHeader(short type, int fileSize, int reserved, int offset) /// 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 @@ public BmpInfoHeader( 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 @@ public BmpInfoHeader( 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 @@ public BmpInfoHeader( 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 @@ public BmpInfoHeader( /// /// 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 @@ public BmpInfoHeader( /// 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 @@ public static BmpInfoHeader ParseCore(ReadOnlySpan data) /// 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 @@ public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) /// 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 @@ public static BmpInfoHeader ParseV3(ReadOnlySpan data) 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 @@ public static BmpInfoHeader ParseV3(ReadOnlySpan data) /// 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 @@ public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha 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 @@ public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan data) /// 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 @@ public void WriteV3Header(Span buffer) /// /// 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/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 @@ public class BmpDecoderTests 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 @@ public void Encode_8BitColor_WithOctreeQuantizer(TestImageProvider(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 @@ public void Identify_DetectsCorrectBitmapInfoHeaderType(string imagePath, BmpInf 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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index fa51fb2254..d36cec630d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -379,6 +379,7 @@ public static class Bmp 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