diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index f88e5762d2..26687ff16f 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1200,7 +1200,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void ReadInfoHeader() { Span buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize]; - var infoHeaderStart = this.stream.Position; + long infoHeaderStart = this.stream.Position; // Resolution is stored in PPM. this.metadata = new ImageMetadata diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 6384074df3..32025f69fc 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, metadata, 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,78 @@ 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 metadata. + /// The buffer. + private void WriteColorProfile(Stream stream, ImageMetadata metadata, Span buffer) + { + if (metadata.IccProfile != null) { - colorPaletteSize = ColorPaletteSize1Bit; + int streamPositionAfterImageData = (int)stream.Position; + stream.Write(metadata.IccProfile.ToByteArray()); + 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/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 9b6e4e6524..31394821f8 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -532,6 +532,17 @@ namespace SixLabors.ImageSharp.Formats.Bmp 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)); + + dest = this; + } + internal void VerifyDimensions() { const int MaximumBmpDimension = 65535; diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs index 8e03625959..e437a0cbf8 100644 --- a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs +++ b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp Invalid = 0, /// - /// TMaintains saturation. Used for business charts and other situations in which undithered colors are required. + /// Maintains saturation. Used for business charts and other situations in which undithered colors are required. /// LCS_GM_BUSINESS = 1,