Bug 1615394 - Add color management support to the BMP decoder. r=tnikkel

This patch adds support for color management in BMPs added in
BITMAPV4HEADER and BITMAPV5HEADER. While display of BMPs is relatively
rare this comes in handy when interacting with the Windows clipboard.

Differential Revision: https://phabricator.services.mozilla.com/D64880

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Andrew Osmond 2020-03-03 14:16:00 +00:00
Родитель 939aa616ca
Коммит 5ee82cb26c
9 изменённых файлов: 366 добавлений и 52 удалений

Просмотреть файл

@ -903,10 +903,12 @@ static struct curveType *curve_from_gamma(float gamma)
//XXX: should this also be taking a black_point?
/* similar to CGColorSpaceCreateCalibratedRGB */
qcms_profile* qcms_profile_create_rgb_with_gamma(
qcms_profile* qcms_profile_create_rgb_with_gamma_set(
qcms_CIE_xyY white_point,
qcms_CIE_xyYTRIPLE primaries,
float gamma)
float redGamma,
float greenGamma,
float blueGamma)
{
qcms_profile* profile = qcms_profile_create();
if (!profile)
@ -918,9 +920,9 @@ qcms_profile* qcms_profile_create_rgb_with_gamma(
return INVALID_PROFILE;
}
profile->redTRC = curve_from_gamma(gamma);
profile->blueTRC = curve_from_gamma(gamma);
profile->greenTRC = curve_from_gamma(gamma);
profile->redTRC = curve_from_gamma(redGamma);
profile->blueTRC = curve_from_gamma(blueGamma);
profile->greenTRC = curve_from_gamma(greenGamma);
if (!profile->redTRC || !profile->blueTRC || !profile->greenTRC) {
qcms_profile_release(profile);
@ -933,6 +935,14 @@ qcms_profile* qcms_profile_create_rgb_with_gamma(
return profile;
}
qcms_profile* qcms_profile_create_rgb_with_gamma(
qcms_CIE_xyY white_point,
qcms_CIE_xyYTRIPLE primaries,
float gamma)
{
return qcms_profile_create_rgb_with_gamma_set(white_point, primaries, gamma, gamma, gamma);
}
qcms_profile* qcms_profile_create_rgb_with_table(
qcms_CIE_xyY white_point,
qcms_CIE_xyYTRIPLE primaries,
@ -1016,6 +1026,11 @@ static qcms_CIE_xyY white_point_from_temp(int temp_K)
return white_point;
}
qcms_CIE_xyY qcms_white_point_sRGB(void)
{
return white_point_from_temp(6504);
}
qcms_profile* qcms_profile_sRGB(void)
{
qcms_profile *profile;
@ -1028,7 +1043,7 @@ qcms_profile* qcms_profile_sRGB(void)
};
qcms_CIE_xyY D65;
D65 = white_point_from_temp(6504);
D65 = qcms_white_point_sRGB();
table = build_sRGB_gamma_table(1024);

Просмотреть файл

@ -129,6 +129,13 @@ typedef struct
qcms_CIE_xyY blue;
} qcms_CIE_xyYTRIPLE;
qcms_profile* qcms_profile_create_rgb_with_gamma_set(
qcms_CIE_xyY white_point,
qcms_CIE_xyYTRIPLE primaries,
float redGamma,
float blueGamma,
float greenGamma);
qcms_profile* qcms_profile_create_rgb_with_gamma(
qcms_CIE_xyY white_point,
qcms_CIE_xyYTRIPLE primaries,
@ -152,7 +159,10 @@ void qcms_data_from_path(const char *path, void **mem, size_t *size);
qcms_profile* qcms_profile_from_unicode_path(const wchar_t *path);
void qcms_data_from_unicode_path(const wchar_t *path, void **mem, size_t *size);
#endif
qcms_CIE_xyY qcms_white_point_sRGB(void);
qcms_profile* qcms_profile_sRGB(void);
void qcms_profile_release(qcms_profile *profile);
bool qcms_profile_is_bogus(qcms_profile *profile);

Просмотреть файл

@ -31,6 +31,21 @@ struct InfoHeaderLength {
};
};
enum class InfoColorSpace : uint32_t {
CALIBRATED_RGB = 0x00000000,
SRGB = 0x73524742,
WINDOWS = 0x57696E20,
LINKED = 0x4C494E4B,
EMBEDDED = 0x4D424544,
};
enum class InfoColorIntent : uint32_t {
BUSINESS = 0x00000001,
GRAPHICS = 0x00000002,
IMAGES = 0x00000004,
ABS_COLORIMETRIC = 0x00000008,
};
} // namespace bmp
} // namespace image
} // namespace mozilla

Просмотреть файл

@ -434,6 +434,7 @@ class Decoder {
protected:
friend class AutoRecordDecoderTelemetry;
friend class DecoderTestHelper;
friend class nsBMPDecoder;
friend class nsICODecoder;
friend class PalettedSurfaceSink;
friend class SurfaceSink;

Просмотреть файл

@ -103,6 +103,7 @@
#include "RasterImage.h"
#include "SurfacePipeFactory.h"
#include "gfxPlatform.h"
#include <algorithm>
using namespace mozilla::gfx;
@ -132,6 +133,32 @@ struct RLE {
using namespace bmp;
static double FixedPoint2Dot30_To_Double(uint32_t aFixed) {
constexpr double factor = 1.0 / 1073741824.0; // 2^-30
return double(aFixed) * factor;
}
static float FixedPoint16Dot16_To_Float(uint32_t aFixed) {
constexpr double factor = 1.0 / 65536.0; // 2^-16
return double(aFixed) * factor;
}
static float CalRbgEndpointToQcms(const CalRgbEndpoint& aIn,
qcms_CIE_xyY& aOut) {
aOut.x = FixedPoint2Dot30_To_Double(aIn.mX);
aOut.y = FixedPoint2Dot30_To_Double(aIn.mY);
aOut.Y = FixedPoint2Dot30_To_Double(aIn.mZ);
return FixedPoint16Dot16_To_Float(aIn.mGamma);
}
static void ReadCalRgbEndpoint(const char* aData, uint32_t aEndpointOffset,
uint32_t aGammaOffset, CalRgbEndpoint& aOut) {
aOut.mX = LittleEndian::readUint32(aData + aEndpointOffset);
aOut.mY = LittleEndian::readUint32(aData + aEndpointOffset + 4);
aOut.mZ = LittleEndian::readUint32(aData + aEndpointOffset + 8);
aOut.mGamma = LittleEndian::readUint32(aData + aGammaOffset);
}
/// Sets the pixel data in aDecoded to the given values.
/// @param aDecoded pointer to pixel to be set, will be incremented to point to
/// the next pixel.
@ -400,35 +427,45 @@ LexerResult nsBMPDecoder::DoDecode(SourceBufferIterator& aIterator,
IResumable* aOnResume) {
MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
return mLexer.Lex(aIterator, aOnResume,
[=](State aState, const char* aData, size_t aLength) {
switch (aState) {
case State::FILE_HEADER:
return ReadFileHeader(aData, aLength);
case State::INFO_HEADER_SIZE:
return ReadInfoHeaderSize(aData, aLength);
case State::INFO_HEADER_REST:
return ReadInfoHeaderRest(aData, aLength);
case State::BITFIELDS:
return ReadBitfields(aData, aLength);
case State::COLOR_TABLE:
return ReadColorTable(aData, aLength);
case State::GAP:
return SkipGap();
case State::AFTER_GAP:
return AfterGap();
case State::PIXEL_ROW:
return ReadPixelRow(aData);
case State::RLE_SEGMENT:
return ReadRLESegment(aData);
case State::RLE_DELTA:
return ReadRLEDelta(aData);
case State::RLE_ABSOLUTE:
return ReadRLEAbsolute(aData, aLength);
default:
MOZ_CRASH("Unknown State");
}
});
return mLexer.Lex(
aIterator, aOnResume,
[=](State aState, const char* aData, size_t aLength) {
switch (aState) {
case State::FILE_HEADER:
return ReadFileHeader(aData, aLength);
case State::INFO_HEADER_SIZE:
return ReadInfoHeaderSize(aData, aLength);
case State::INFO_HEADER_REST:
return ReadInfoHeaderRest(aData, aLength);
case State::BITFIELDS:
return ReadBitfields(aData, aLength);
case State::SKIP_TO_COLOR_PROFILE:
return Transition::ContinueUnbuffered(State::SKIP_TO_COLOR_PROFILE);
case State::FOUND_COLOR_PROFILE:
return Transition::To(State::COLOR_PROFILE,
mH.mColorSpace.mProfile.mLength);
case State::COLOR_PROFILE:
return ReadColorProfile(aData, aLength);
case State::ALLOCATE_SURFACE:
return AllocateSurface();
case State::COLOR_TABLE:
return ReadColorTable(aData, aLength);
case State::GAP:
return SkipGap();
case State::AFTER_GAP:
return AfterGap();
case State::PIXEL_ROW:
return ReadPixelRow(aData);
case State::RLE_SEGMENT:
return ReadRLESegment(aData);
case State::RLE_DELTA:
return ReadRLEDelta(aData);
case State::RLE_ABSOLUTE:
return ReadRLEAbsolute(aData, aLength);
default:
MOZ_CRASH("Unknown State");
}
});
}
LexerTransition<nsBMPDecoder::State> nsBMPDecoder::ReadFileHeader(
@ -497,6 +534,43 @@ LexerTransition<nsBMPDecoder::State> nsBMPDecoder::ReadInfoHeaderRest(
mH.mNumColors = aLength >= 32 ? LittleEndian::readUint32(aData + 28) : 0;
// We ignore the important_colors (aData + 36) field.
// Read color management properties we may need later.
mH.mCsType =
aLength >= 56
? static_cast<InfoColorSpace>(LittleEndian::readUint32(aData + 52))
: InfoColorSpace::SRGB;
mH.mCsIntent = aLength >= 108 ? static_cast<InfoColorIntent>(
LittleEndian::readUint32(aData + 104))
: InfoColorIntent::IMAGES;
switch (mH.mCsType) {
case InfoColorSpace::CALIBRATED_RGB:
if (aLength >= 104) {
ReadCalRgbEndpoint(aData, 56, 92, mH.mColorSpace.mCalibrated.mRed);
ReadCalRgbEndpoint(aData, 68, 96, mH.mColorSpace.mCalibrated.mGreen);
ReadCalRgbEndpoint(aData, 80, 100, mH.mColorSpace.mCalibrated.mBlue);
} else {
mH.mCsType = InfoColorSpace::SRGB;
}
break;
case InfoColorSpace::EMBEDDED:
if (aLength >= 116) {
mH.mColorSpace.mProfile.mOffset =
LittleEndian::readUint32(aData + 108);
mH.mColorSpace.mProfile.mLength =
LittleEndian::readUint32(aData + 112);
} else {
mH.mCsType = InfoColorSpace::SRGB;
}
break;
case InfoColorSpace::LINKED:
case InfoColorSpace::SRGB:
case InfoColorSpace::WINDOWS:
default:
// Nothing to be done at this time.
break;
}
// For WinBMPv4, WinBMPv5 and (possibly) OS2-BMPv2 there are additional
// fields in the info header which we ignore, with the possible exception
// of the color bitfields (see below).
@ -643,6 +717,156 @@ LexerTransition<nsBMPDecoder::State> nsBMPDecoder::ReadBitfields(
mBytesPerColor = (mH.mBIHSize == InfoHeaderLength::WIN_V2) ? 3 : 4;
}
auto cmsMode = gfxPlatform::GetCMSMode();
if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) {
cmsMode = eCMSMode_Off;
}
if (cmsMode != eCMSMode_Off) {
switch (mH.mCsType) {
case InfoColorSpace::EMBEDDED:
return SeekColorProfile(aLength);
case InfoColorSpace::CALIBRATED_RGB:
PrepareCalibratedColorProfile();
break;
case InfoColorSpace::SRGB:
case InfoColorSpace::WINDOWS:
MOZ_LOG(sBMPLog, LogLevel::Debug, ("using sRGB color profile\n"));
if (mColors) {
// We will transform the color table instead of the output pixels.
mTransform = gfxPlatform::GetCMSRGBTransform();
} else {
mTransform = gfxPlatform::GetCMSOSRGBATransform();
}
break;
case InfoColorSpace::LINKED:
default:
// Not supported, no color management.
MOZ_LOG(sBMPLog, LogLevel::Debug, ("color space type not provided\n"));
break;
}
}
return Transition::To(State::ALLOCATE_SURFACE, 0);
}
void nsBMPDecoder::PrepareCalibratedColorProfile() {
// BMP does not define a white point. Use the same as sRGB. This matches what
// Chrome does as well.
qcms_CIE_xyY white_point = qcms_white_point_sRGB();
qcms_CIE_xyYTRIPLE primaries;
float redGamma =
CalRbgEndpointToQcms(mH.mColorSpace.mCalibrated.mRed, primaries.red);
float greenGamma =
CalRbgEndpointToQcms(mH.mColorSpace.mCalibrated.mGreen, primaries.green);
float blueGamma =
CalRbgEndpointToQcms(mH.mColorSpace.mCalibrated.mBlue, primaries.blue);
// Explicitly verify the profile because sometimes the values from the BMP
// header are just garbage.
mInProfile = qcms_profile_create_rgb_with_gamma_set(
white_point, primaries, redGamma, greenGamma, blueGamma);
if (mInProfile && qcms_profile_is_bogus(mInProfile)) {
// Bad profile, just use sRGB instead. Release the profile here, so that
// our destructor doesn't assume we are the owner for the transform.
qcms_profile_release(mInProfile);
mInProfile = nullptr;
}
if (mInProfile) {
MOZ_LOG(sBMPLog, LogLevel::Debug, ("using calibrated RGB color profile\n"));
PrepareColorProfileTransform();
} else {
MOZ_LOG(sBMPLog, LogLevel::Debug,
("failed to create calibrated RGB color profile, using sRGB\n"));
if (mColors) {
// We will transform the color table instead of the output pixels.
mTransform = gfxPlatform::GetCMSRGBTransform();
} else {
mTransform = gfxPlatform::GetCMSOSRGBATransform();
}
}
}
void nsBMPDecoder::PrepareColorProfileTransform() {
if (!mInProfile || !gfxPlatform::GetCMSOutputProfile()) {
return;
}
qcms_data_type inType;
qcms_data_type outType;
if (mColors) {
// We will transform the color table instead of the output pixels.
inType = QCMS_DATA_RGB_8;
outType = QCMS_DATA_RGB_8;
} else {
inType = gfxPlatform::GetCMSOSRGBAType();
outType = inType;
}
qcms_intent intent;
switch (mH.mCsIntent) {
case InfoColorIntent::BUSINESS:
intent = QCMS_INTENT_SATURATION;
break;
case InfoColorIntent::GRAPHICS:
intent = QCMS_INTENT_RELATIVE_COLORIMETRIC;
break;
case InfoColorIntent::ABS_COLORIMETRIC:
intent = QCMS_INTENT_ABSOLUTE_COLORIMETRIC;
break;
case InfoColorIntent::IMAGES:
default:
intent = QCMS_INTENT_PERCEPTUAL;
break;
}
mTransform = qcms_transform_create(
mInProfile, inType, gfxPlatform::GetCMSOutputProfile(), outType, intent);
if (!mTransform) {
MOZ_LOG(sBMPLog, LogLevel::Debug,
("failed to create color profile transform\n"));
}
}
LexerTransition<nsBMPDecoder::State> nsBMPDecoder::SeekColorProfile(
size_t aLength) {
// The offset needs to be at least after the color table.
uint32_t offset = mH.mColorSpace.mProfile.mOffset;
if (offset <= mH.mBIHSize + aLength + mNumColors * mBytesPerColor ||
mH.mColorSpace.mProfile.mLength == 0) {
return Transition::To(State::ALLOCATE_SURFACE, 0);
}
// We have already read the header and bitfields.
offset -= mH.mBIHSize + aLength;
// We need to skip ahead to search for the embedded color profile. We want
// to return to this point once we read it.
mReturnIterator = mLexer.Clone(*mIterator, SIZE_MAX);
if (!mReturnIterator) {
return Transition::TerminateFailure();
}
return Transition::ToUnbuffered(State::FOUND_COLOR_PROFILE,
State::SKIP_TO_COLOR_PROFILE, offset);
}
LexerTransition<nsBMPDecoder::State> nsBMPDecoder::ReadColorProfile(
const char* aData, size_t aLength) {
mInProfile = qcms_profile_from_memory(aData, aLength);
if (mInProfile) {
MOZ_LOG(sBMPLog, LogLevel::Debug, ("using embedded color profile\n"));
PrepareColorProfileTransform();
}
// Jump back to where we left off.
mIterator = std::move(mReturnIterator);
return Transition::To(State::ALLOCATE_SURFACE, 0);
}
LexerTransition<nsBMPDecoder::State> nsBMPDecoder::AllocateSurface() {
SurfaceFormat format;
SurfacePipeFlags pipeFlags = SurfacePipeFlags();
@ -665,9 +889,13 @@ LexerTransition<nsBMPDecoder::State> nsBMPDecoder::ReadBitfields(
return Transition::TerminateFailure();
}
// Only give the color transform to the SurfacePipe if we are not transforming
// the color table in advance.
qcms_transform* transform = mColors ? nullptr : mTransform;
Maybe<SurfacePipe> pipe = SurfacePipeFactory::CreateSurfacePipe(
this, Size(), OutputSize(), FullFrame(), format, format, Nothing(),
mTransform, pipeFlags);
transform, pipeFlags);
if (!pipe) {
return Transition::TerminateFailure();
}
@ -691,6 +919,14 @@ LexerTransition<nsBMPDecoder::State> nsBMPDecoder::ReadColorTable(
aData += mBytesPerColor;
}
// If we have a color table and a transform, we can avoid transforming each
// pixel by doing the table in advance. We color manage every entry in the
// table, even if it is smaller in case the BMP is malformed and overruns
// its stated color range.
if (mColors && mTransform) {
qcms_transform_data(mTransform, mColors.get(), mColors.get(), 256);
}
// If we are decoding a BMP from the clipboard, we did not know the data
// offset in advance. It is just defined as after the header and color table.
if (mIsForClipboard) {

Просмотреть файл

@ -19,19 +19,41 @@ namespace image {
namespace bmp {
struct CalRgbEndpoint {
uint32_t mGamma;
uint32_t mX;
uint32_t mY;
uint32_t mZ;
};
/// This struct contains the fields from the file header and info header that
/// we use during decoding. (Excluding bitfields fields, which are kept in
/// BitFields.)
struct Header {
uint32_t mDataOffset; // Offset to raster data.
uint32_t mBIHSize; // Header size.
int32_t mWidth; // Image width.
int32_t mHeight; // Image height.
uint16_t mBpp; // Bits per pixel.
uint32_t mCompression; // See struct Compression for valid values.
uint32_t mImageSize; // (compressed) image size. Can be 0 if
// mCompression==0.
uint32_t mNumColors; // Used colors.
uint32_t mDataOffset; // Offset to raster data.
uint32_t mBIHSize; // Header size.
int32_t mWidth; // Image width.
int32_t mHeight; // Image height.
uint16_t mBpp; // Bits per pixel.
uint32_t mCompression; // See struct Compression for valid values.
uint32_t mImageSize; // (compressed) image size. Can be 0 if
// mCompression==0.
uint32_t mNumColors; // Used colors.
InfoColorSpace mCsType; // Color space type.
InfoColorIntent mCsIntent; // Color space intent.
union {
struct {
CalRgbEndpoint mRed;
CalRgbEndpoint mGreen;
CalRgbEndpoint mBlue;
} mCalibrated;
struct {
uint32_t mOffset;
uint32_t mLength;
} mProfile;
} mColorSpace;
Header()
: mDataOffset(0),
@ -41,7 +63,9 @@ struct Header {
mBpp(0),
mCompression(0),
mImageSize(0),
mNumColors(0) {}
mNumColors(0),
mCsType(InfoColorSpace::SRGB),
mCsIntent(InfoColorIntent::IMAGES) {}
};
/// An entry in the color table.
@ -158,6 +182,10 @@ class nsBMPDecoder : public Decoder {
INFO_HEADER_SIZE,
INFO_HEADER_REST,
BITFIELDS,
SKIP_TO_COLOR_PROFILE,
FOUND_COLOR_PROFILE,
COLOR_PROFILE,
ALLOCATE_SURFACE,
COLOR_TABLE,
GAP,
AFTER_GAP,
@ -184,10 +212,16 @@ class nsBMPDecoder : public Decoder {
void FinishRow();
void PrepareCalibratedColorProfile();
void PrepareColorProfileTransform();
LexerTransition<State> ReadFileHeader(const char* aData, size_t aLength);
LexerTransition<State> ReadInfoHeaderSize(const char* aData, size_t aLength);
LexerTransition<State> ReadInfoHeaderRest(const char* aData, size_t aLength);
LexerTransition<State> ReadBitfields(const char* aData, size_t aLength);
LexerTransition<State> SeekColorProfile(size_t aLength);
LexerTransition<State> ReadColorProfile(const char* aData, size_t aLength);
LexerTransition<State> AllocateSurface();
LexerTransition<State> ReadColorTable(const char* aData, size_t aLength);
LexerTransition<State> SkipGap();
LexerTransition<State> AfterGap();
@ -200,6 +234,9 @@ class nsBMPDecoder : public Decoder {
StreamingLexer<State> mLexer;
// Iterator to save return point.
Maybe<SourceBufferIterator> mReturnIterator;
UniquePtr<uint32_t[]> mRowBuffer;
bmp::Header mH;

Просмотреть файл

@ -73,7 +73,8 @@ fuzzy(0-1,0-996) == pal8os2.bmp pal8.png
# BMP: bihsize=108, 127 x 64, bpp=8, compression=0, colors=252
# "A v4 bitmap. Im not sure that the gamma and chromaticity values in this
# file are sensible, because I cant find any detailed documentation of them."
fuzzy(0-1,0-996) == pal8v4.bmp pal8.png
# [We seem to handle the profile wrong in QCMS. See bug 1619332.]
fuzzy(3-3,6376-6376) == pal8v4.bmp pal8.png
# BMP: bihsize=124, 127 x 64, bpp=8, compression=0, colors=252
# "A v5 bitmap. Version 5 has additional colorspace options over v4, so it is

Просмотреть файл

@ -151,16 +151,15 @@ fuzzy(0-1,0-2203) == rgba16-5551.bmp rgba16-5551.png
# BMP: bihsize=124, 127 x 64, bpp=24, compression=0, colors=0
# "My attempt to make a BMP file with an embedded color profile."
# [We support it, though we don't do anything with the color profile. Chromium
# also handles it.]
== rgb24prof.bmp rgb24.png
fuzzy(1-1,28-28) == rgb24prof.bmp rgb24.png
# BMP: bihsize=124, 127 x 64, bpp=24, compression=0, colors=0
# "This image tries to test whether color profiles are fully supported. It has
# the red and green channels swapped, and an embedded color profile that tries
# to swap them back. Support for this is uncommon."
# [We don't match rgb24.png as per bmpsuite, but we do match Chrome.]
== rgb24prof2.bmp rgb24prof2.png
# [The image is significantly closer to the desired output than without color
# management, but we seem to handle the profile wrong in QCMS. See bug 1619332.]
fuzzy(10-10,6597-6597) == rgb24prof2.bmp rgb24.png
# BMP: bihsize=124, 127 x 64, bpp=24, compression=0, colors=0
# "My attempt to make a BMP file with a linked color profile."

Двоичные данные
image/test/reftest/bmp/bmpsuite/q/rgb24prof2.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 19 KiB