Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve color profile when encoding bitmaps #2108

Merged
merged 5 commits into from
May 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/ImageSharp/Formats/Bmp/BmpColorSpace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Bmp
{
/// <summary>
/// Enum for the different color spaces.
/// </summary>
internal enum BmpColorSpace
{
/// <summary>
/// This value implies that endpoints and gamma values are given in the appropriate fields.
/// </summary>
LCS_CALIBRATED_RGB = 0,

/// <summary>
/// The Windows default color space ('Win ').
/// </summary>
LCS_WINDOWS_COLOR_SPACE = 1466527264,

/// <summary>
/// Specifies that the bitmap is in sRGB color space ('sRGB').
/// </summary>
LCS_sRGB = 1934772034,

/// <summary>
/// This value indicates that bV5ProfileData points to the file name of the profile to use (gamma and endpoints values are ignored).
/// </summary>
PROFILE_LINKED = 1279872587,

/// <summary>
/// This value indicates that bV5ProfileData points to a memory buffer that contains the profile to be used (gamma and endpoints values are ignored).
/// </summary>
PROFILE_EMBEDDED = 1296188740
}
}
53 changes: 34 additions & 19 deletions src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -185,7 +186,7 @@ public Image<TPixel> Decode<TPixel>(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;
}
Expand Down Expand Up @@ -1199,6 +1200,13 @@ private static int CountBits(uint n)
private void ReadInfoHeader()
{
Span<byte> buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize];
long infoHeaderStart = this.stream.Position;

// Resolution is stored in PPM.
this.metadata = new ImageMetadata
{
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
};

// Read the header size.
this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
150 changes: 120 additions & 30 deletions src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
Expand Down Expand Up @@ -79,9 +80,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <summary>
/// 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.
/// </summary>
private readonly bool writeV4Header;
private BmpInfoHeaderType infoHeaderType;

/// <summary>
/// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images.
Expand All @@ -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;
}

/// <summary>
Expand All @@ -123,7 +125,62 @@ public void Encode<TPixel>(Image<TPixel> 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<byte> buffer = stackalloc byte[infoHeaderSize];

this.WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(stream, image.Frames.RootFrame);
this.WriteColorProfile(stream, iccProfileData, buffer);

stream.Flush();
}

/// <summary>
/// Creates the bitmap information header.
/// </summary>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="infoHeaderSize">Size of the information header.</param>
/// <param name="bpp">The bits per pixel.</param>
/// <param name="bytesPerLine">The bytes per line.</param>
/// <param name="metadata">The metadata.</param>
/// <param name="iccProfileData">The icc profile data.</param>
/// <returns>The bitmap information header.</returns>
private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, short bpp, int bytesPerLine, ImageMetadata metadata, byte[] iccProfileData)
{
int hResolution = 0;
int vResolution = 0;

Expand Down Expand Up @@ -154,20 +211,19 @@ public void Encode<TPixel>(Image<TPixel> 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;
Expand All @@ -176,45 +232,79 @@ public void Encode<TPixel>(Image<TPixel> 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;
}

/// <summary>
/// Writes the color profile to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileData">The color profile data.</param>
/// <param name="buffer">The buffer.</param>
private void WriteColorProfile(Stream stream, byte[] iccProfileData, Span<byte> buffer)
{
if (iccProfileData != null)
{
colorPaletteSize = ColorPaletteSize1Bit;
// The offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data.
int streamPositionAfterImageData = (int)stream.Position - BmpFileHeader.Size;
stream.Write(iccProfileData);
BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData);
stream.Position = BmpFileHeader.Size + 112;
stream.Write(buffer.Slice(0, 4));
}
}

/// <summary>
/// Writes the bitmap file header.
/// </summary>
/// <param name="stream">The stream to write the header to.</param>
/// <param name="infoHeaderSize">Size of the bitmap information header.</param>
/// <param name="colorPaletteSize">Size of the color palette.</param>
/// <param name="iccProfileSize">The size in bytes of the color profile.</param>
/// <param name="infoHeader">The information header to write.</param>
/// <param name="buffer">The buffer to write to.</param>
private void WriteBitmapFileHeader(Stream stream, int infoHeaderSize, int colorPaletteSize, int iccProfileSize, BmpInfoHeader infoHeader, Span<byte> buffer)
{
var fileHeader = new BmpFileHeader(
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<byte> buffer = stackalloc byte[infoHeaderSize];
fileHeader.WriteTo(buffer);

stream.Write(buffer, 0, BmpFileHeader.Size);
}

if (this.writeV4Header)
{
infoHeader.WriteV4Header(buffer);
}
else
/// <summary>
/// Writes the bitmap information header.
/// </summary>
/// <param name="stream">The stream to write info header into.</param>
/// <param name="infoHeader">The information header.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="infoHeaderSize">Size of the information header.</param>
private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span<byte> buffer, int infoHeaderSize)
{
switch (this.infoHeaderType)
{
infoHeader.WriteV3Header(buffer);
case BmpInfoHeaderType.WinVersion3:
infoHeader.WriteV3Header(buffer);
break;
case BmpInfoHeaderType.WinVersion4:
infoHeader.WriteV4Header(buffer);
break;
case BmpInfoHeaderType.WinVersion5:
infoHeader.WriteV5Header(buffer);
break;
}

stream.Write(buffer, 0, infoHeaderSize);

this.WriteImage(stream, image.Frames.RootFrame);

stream.Flush();
}

/// <summary>
Expand Down
5 changes: 1 addition & 4 deletions src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ public BmpFileHeader(short type, int fileSize, int reserved, int offset)
/// </summary>
public int Offset { get; }

public static BmpFileHeader Parse(Span<byte> data)
{
return MemoryMarshal.Cast<byte, BmpFileHeader>(data)[0];
}
public static BmpFileHeader Parse(Span<byte> data) => MemoryMarshal.Cast<byte, BmpFileHeader>(data)[0];

public void WriteTo(Span<byte> buffer)
{
Expand Down
Loading