Skip to content

Commit 2aa150c

Browse files
authored
Merge pull request #2108 from SixLabors/bp/bmp-icc
Preserve color profile when encoding bitmaps
2 parents d2bad1f + 5388489 commit 2aa150c

11 files changed

+388
-78
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
// ReSharper disable InconsistentNaming
5+
namespace SixLabors.ImageSharp.Formats.Bmp
6+
{
7+
/// <summary>
8+
/// Enum for the different color spaces.
9+
/// </summary>
10+
internal enum BmpColorSpace
11+
{
12+
/// <summary>
13+
/// This value implies that endpoints and gamma values are given in the appropriate fields.
14+
/// </summary>
15+
LCS_CALIBRATED_RGB = 0,
16+
17+
/// <summary>
18+
/// The Windows default color space ('Win ').
19+
/// </summary>
20+
LCS_WINDOWS_COLOR_SPACE = 1466527264,
21+
22+
/// <summary>
23+
/// Specifies that the bitmap is in sRGB color space ('sRGB').
24+
/// </summary>
25+
LCS_sRGB = 1934772034,
26+
27+
/// <summary>
28+
/// This value indicates that bV5ProfileData points to the file name of the profile to use (gamma and endpoints values are ignored).
29+
/// </summary>
30+
PROFILE_LINKED = 1279872587,
31+
32+
/// <summary>
33+
/// This value indicates that bV5ProfileData points to a memory buffer that contains the profile to be used (gamma and endpoints values are ignored).
34+
/// </summary>
35+
PROFILE_EMBEDDED = 1296188740
36+
}
37+
}

src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

+34-19
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using SixLabors.ImageSharp.IO;
1212
using SixLabors.ImageSharp.Memory;
1313
using SixLabors.ImageSharp.Metadata;
14+
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
1415
using SixLabors.ImageSharp.PixelFormats;
1516

1617
namespace SixLabors.ImageSharp.Formats.Bmp
@@ -185,7 +186,7 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
185186
break;
186187

187188
default:
188-
BmpThrowHelper.ThrowNotSupportedException("Does not support this kind of bitmap files.");
189+
BmpThrowHelper.ThrowNotSupportedException("ImageSharp does not support this kind of bitmap files.");
189190

190191
break;
191192
}
@@ -1199,6 +1200,13 @@ private static int CountBits(uint n)
11991200
private void ReadInfoHeader()
12001201
{
12011202
Span<byte> buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize];
1203+
long infoHeaderStart = this.stream.Position;
1204+
1205+
// Resolution is stored in PPM.
1206+
this.metadata = new ImageMetadata
1207+
{
1208+
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
1209+
};
12021210

12031211
// Read the header size.
12041212
this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);
@@ -1271,36 +1279,45 @@ private void ReadInfoHeader()
12711279
infoHeaderType = BmpInfoHeaderType.Os2Version2;
12721280
this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer);
12731281
}
1274-
else if (headerSize >= BmpInfoHeader.SizeV4)
1282+
else if (headerSize == BmpInfoHeader.SizeV4)
12751283
{
1276-
// >= 108 bytes
1277-
infoHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5;
1284+
// == 108 bytes
1285+
infoHeaderType = BmpInfoHeaderType.WinVersion4;
12781286
this.infoHeader = BmpInfoHeader.ParseV4(buffer);
12791287
}
1288+
else if (headerSize > BmpInfoHeader.SizeV4)
1289+
{
1290+
// > 108 bytes
1291+
infoHeaderType = BmpInfoHeaderType.WinVersion5;
1292+
this.infoHeader = BmpInfoHeader.ParseV5(buffer);
1293+
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
1294+
{
1295+
// Read color profile.
1296+
long streamPosition = this.stream.Position;
1297+
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
1298+
this.stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
1299+
this.stream.Read(iccProfileData);
1300+
this.metadata.IccProfile = new IccProfile(iccProfileData);
1301+
this.stream.Position = streamPosition;
1302+
}
1303+
}
12801304
else
12811305
{
12821306
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'.");
12831307
}
12841308

1285-
// Resolution is stored in PPM.
1286-
var meta = new ImageMetadata
1287-
{
1288-
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
1289-
};
12901309
if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0)
12911310
{
1292-
meta.HorizontalResolution = this.infoHeader.XPelsPerMeter;
1293-
meta.VerticalResolution = this.infoHeader.YPelsPerMeter;
1311+
this.metadata.HorizontalResolution = this.infoHeader.XPelsPerMeter;
1312+
this.metadata.VerticalResolution = this.infoHeader.YPelsPerMeter;
12941313
}
12951314
else
12961315
{
12971316
// Convert default metadata values to PPM.
1298-
meta.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution));
1299-
meta.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution));
1317+
this.metadata.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution));
1318+
this.metadata.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution));
13001319
}
13011320

1302-
this.metadata = meta;
1303-
13041321
short bitsPerPixel = this.infoHeader.BitsPerPixel;
13051322
this.bmpMetadata = this.metadata.GetBmpMetadata();
13061323
this.bmpMetadata.InfoHeaderType = infoHeaderType;
@@ -1370,9 +1387,7 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b
13701387
int colorMapSizeBytes = -1;
13711388
if (this.infoHeader.ClrUsed == 0)
13721389
{
1373-
if (this.infoHeader.BitsPerPixel == 1
1374-
|| this.infoHeader.BitsPerPixel == 4
1375-
|| this.infoHeader.BitsPerPixel == 8)
1390+
if (this.infoHeader.BitsPerPixel is 1 or 4 or 8)
13761391
{
13771392
switch (this.fileMarkerType)
13781393
{
@@ -1424,7 +1439,7 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b
14241439
int skipAmount = this.fileHeader.Offset - (int)this.stream.Position;
14251440
if ((skipAmount + (int)this.stream.Position) > this.stream.Length)
14261441
{
1427-
BmpThrowHelper.ThrowInvalidImageContentException("Invalid fileheader offset found. Offset is greater than the stream length.");
1442+
BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length.");
14281443
}
14291444

14301445
if (skipAmount > 0)

src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

+120-30
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Buffers;
6+
using System.Buffers.Binary;
67
using System.IO;
78
using System.Runtime.InteropServices;
89
using System.Threading;
@@ -79,9 +80,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
7980
/// <summary>
8081
/// A bitmap v4 header will only be written, if the user explicitly wants support for transparency.
8182
/// In this case the compression type BITFIELDS will be used.
83+
/// If the image contains a color profile, a bitmap v5 header is written, which is needed to write this info.
8284
/// Otherwise a bitmap v3 header will be written, which is supported by almost all decoders.
8385
/// </summary>
84-
private readonly bool writeV4Header;
86+
private BmpInfoHeaderType infoHeaderType;
8587

8688
/// <summary>
8789
/// 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
9799
{
98100
this.memoryAllocator = memoryAllocator;
99101
this.bitsPerPixel = options.BitsPerPixel;
100-
this.writeV4Header = options.SupportTransparency;
101102
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
103+
this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
102104
}
103105

104106
/// <summary>
@@ -123,7 +125,62 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
123125
int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32);
124126
this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F));
125127

126-
// Set Resolution.
128+
int colorPaletteSize = 0;
129+
if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8)
130+
{
131+
colorPaletteSize = ColorPaletteSize8Bit;
132+
}
133+
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4)
134+
{
135+
colorPaletteSize = ColorPaletteSize4Bit;
136+
}
137+
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1)
138+
{
139+
colorPaletteSize = ColorPaletteSize1Bit;
140+
}
141+
142+
byte[] iccProfileData = null;
143+
int iccProfileSize = 0;
144+
if (metadata.IccProfile != null)
145+
{
146+
this.infoHeaderType = BmpInfoHeaderType.WinVersion5;
147+
iccProfileData = metadata.IccProfile.ToByteArray();
148+
iccProfileSize = iccProfileData.Length;
149+
}
150+
151+
int infoHeaderSize = this.infoHeaderType switch
152+
{
153+
BmpInfoHeaderType.WinVersion3 => BmpInfoHeader.SizeV3,
154+
BmpInfoHeaderType.WinVersion4 => BmpInfoHeader.SizeV4,
155+
BmpInfoHeaderType.WinVersion5 => BmpInfoHeader.SizeV5,
156+
_ => BmpInfoHeader.SizeV3
157+
};
158+
159+
BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, image.Height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
160+
161+
Span<byte> buffer = stackalloc byte[infoHeaderSize];
162+
163+
this.WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
164+
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
165+
this.WriteImage(stream, image.Frames.RootFrame);
166+
this.WriteColorProfile(stream, iccProfileData, buffer);
167+
168+
stream.Flush();
169+
}
170+
171+
/// <summary>
172+
/// Creates the bitmap information header.
173+
/// </summary>
174+
/// <param name="width">The width of the image.</param>
175+
/// <param name="height">The height of the image.</param>
176+
/// <param name="infoHeaderSize">Size of the information header.</param>
177+
/// <param name="bpp">The bits per pixel.</param>
178+
/// <param name="bytesPerLine">The bytes per line.</param>
179+
/// <param name="metadata">The metadata.</param>
180+
/// <param name="iccProfileData">The icc profile data.</param>
181+
/// <returns>The bitmap information header.</returns>
182+
private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, short bpp, int bytesPerLine, ImageMetadata metadata, byte[] iccProfileData)
183+
{
127184
int hResolution = 0;
128185
int vResolution = 0;
129186

@@ -154,20 +211,19 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
154211
}
155212
}
156213

157-
int infoHeaderSize = this.writeV4Header ? BmpInfoHeader.SizeV4 : BmpInfoHeader.SizeV3;
158214
var infoHeader = new BmpInfoHeader(
159215
headerSize: infoHeaderSize,
160-
height: image.Height,
161-
width: image.Width,
216+
height: height,
217+
width: width,
162218
bitsPerPixel: bpp,
163219
planes: 1,
164-
imageSize: image.Height * bytesPerLine,
220+
imageSize: height * bytesPerLine,
165221
clrUsed: 0,
166222
clrImportant: 0,
167223
xPelsPerMeter: hResolution,
168224
yPelsPerMeter: vResolution);
169225

170-
if (this.writeV4Header && this.bitsPerPixel == BmpBitsPerPixel.Pixel32)
226+
if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Pixel32)
171227
{
172228
infoHeader.AlphaMask = Rgba32AlphaMask;
173229
infoHeader.RedMask = Rgba32RedMask;
@@ -176,45 +232,79 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
176232
infoHeader.Compression = BmpCompression.BitFields;
177233
}
178234

179-
int colorPaletteSize = 0;
180-
if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8)
235+
if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && metadata.IccProfile != null)
181236
{
182-
colorPaletteSize = ColorPaletteSize8Bit;
237+
infoHeader.ProfileSize = iccProfileData.Length;
238+
infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED;
239+
infoHeader.Intent = BmpRenderingIntent.LCS_GM_IMAGES;
183240
}
184-
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4)
185-
{
186-
colorPaletteSize = ColorPaletteSize4Bit;
187-
}
188-
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1)
241+
242+
return infoHeader;
243+
}
244+
245+
/// <summary>
246+
/// Writes the color profile to the stream.
247+
/// </summary>
248+
/// <param name="stream">The stream to write to.</param>
249+
/// <param name="iccProfileData">The color profile data.</param>
250+
/// <param name="buffer">The buffer.</param>
251+
private void WriteColorProfile(Stream stream, byte[] iccProfileData, Span<byte> buffer)
252+
{
253+
if (iccProfileData != null)
189254
{
190-
colorPaletteSize = ColorPaletteSize1Bit;
255+
// The offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data.
256+
int streamPositionAfterImageData = (int)stream.Position - BmpFileHeader.Size;
257+
stream.Write(iccProfileData);
258+
BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData);
259+
stream.Position = BmpFileHeader.Size + 112;
260+
stream.Write(buffer.Slice(0, 4));
191261
}
262+
}
192263

264+
/// <summary>
265+
/// Writes the bitmap file header.
266+
/// </summary>
267+
/// <param name="stream">The stream to write the header to.</param>
268+
/// <param name="infoHeaderSize">Size of the bitmap information header.</param>
269+
/// <param name="colorPaletteSize">Size of the color palette.</param>
270+
/// <param name="iccProfileSize">The size in bytes of the color profile.</param>
271+
/// <param name="infoHeader">The information header to write.</param>
272+
/// <param name="buffer">The buffer to write to.</param>
273+
private void WriteBitmapFileHeader(Stream stream, int infoHeaderSize, int colorPaletteSize, int iccProfileSize, BmpInfoHeader infoHeader, Span<byte> buffer)
274+
{
193275
var fileHeader = new BmpFileHeader(
194276
type: BmpConstants.TypeMarkers.Bitmap,
195-
fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + infoHeader.ImageSize,
277+
fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + iccProfileSize + infoHeader.ImageSize,
196278
reserved: 0,
197279
offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize);
198280

199-
Span<byte> buffer = stackalloc byte[infoHeaderSize];
200281
fileHeader.WriteTo(buffer);
201-
202282
stream.Write(buffer, 0, BmpFileHeader.Size);
283+
}
203284

204-
if (this.writeV4Header)
205-
{
206-
infoHeader.WriteV4Header(buffer);
207-
}
208-
else
285+
/// <summary>
286+
/// Writes the bitmap information header.
287+
/// </summary>
288+
/// <param name="stream">The stream to write info header into.</param>
289+
/// <param name="infoHeader">The information header.</param>
290+
/// <param name="buffer">The buffer.</param>
291+
/// <param name="infoHeaderSize">Size of the information header.</param>
292+
private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span<byte> buffer, int infoHeaderSize)
293+
{
294+
switch (this.infoHeaderType)
209295
{
210-
infoHeader.WriteV3Header(buffer);
296+
case BmpInfoHeaderType.WinVersion3:
297+
infoHeader.WriteV3Header(buffer);
298+
break;
299+
case BmpInfoHeaderType.WinVersion4:
300+
infoHeader.WriteV4Header(buffer);
301+
break;
302+
case BmpInfoHeaderType.WinVersion5:
303+
infoHeader.WriteV5Header(buffer);
304+
break;
211305
}
212306

213307
stream.Write(buffer, 0, infoHeaderSize);
214-
215-
this.WriteImage(stream, image.Frames.RootFrame);
216-
217-
stream.Flush();
218308
}
219309

220310
/// <summary>

src/ImageSharp/Formats/Bmp/BmpFileHeader.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,7 @@ public BmpFileHeader(short type, int fileSize, int reserved, int offset)
5757
/// </summary>
5858
public int Offset { get; }
5959

60-
public static BmpFileHeader Parse(Span<byte> data)
61-
{
62-
return MemoryMarshal.Cast<byte, BmpFileHeader>(data)[0];
63-
}
60+
public static BmpFileHeader Parse(Span<byte> data) => MemoryMarshal.Cast<byte, BmpFileHeader>(data)[0];
6461

6562
public void WriteTo(Span<byte> buffer)
6663
{

0 commit comments

Comments
 (0)