From 5ee82cb26c8af72fbc634cc5995245a067515d38 Mon Sep 17 00:00:00 2001 From: Andrew Osmond Date: Tue, 3 Mar 2020 14:16:00 +0000 Subject: [PATCH] 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 --- gfx/qcms/iccread.c | 27 +- gfx/qcms/qcms.h | 10 + image/BMPHeaders.h | 15 + image/Decoder.h | 1 + image/decoders/nsBMPDecoder.cpp | 296 ++++++++++++++++-- image/decoders/nsBMPDecoder.h | 57 +++- .../test/reftest/bmp/bmpsuite/g/reftest.list | 3 +- .../test/reftest/bmp/bmpsuite/q/reftest.list | 9 +- .../reftest/bmp/bmpsuite/q/rgb24prof2.png | Bin 19650 -> 0 bytes 9 files changed, 366 insertions(+), 52 deletions(-) delete mode 100644 image/test/reftest/bmp/bmpsuite/q/rgb24prof2.png diff --git a/gfx/qcms/iccread.c b/gfx/qcms/iccread.c index bb56ed68d891..8cce93b6150d 100644 --- a/gfx/qcms/iccread.c +++ b/gfx/qcms/iccread.c @@ -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); diff --git a/gfx/qcms/qcms.h b/gfx/qcms/qcms.h index fb69f795b99f..ae889680bc27 100644 --- a/gfx/qcms/qcms.h +++ b/gfx/qcms/qcms.h @@ -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); diff --git a/image/BMPHeaders.h b/image/BMPHeaders.h index 0527f61932d5..544e334e1915 100644 --- a/image/BMPHeaders.h +++ b/image/BMPHeaders.h @@ -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 diff --git a/image/Decoder.h b/image/Decoder.h index 21e0b4966783..64558195a1dc 100644 --- a/image/Decoder.h +++ b/image/Decoder.h @@ -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; diff --git a/image/decoders/nsBMPDecoder.cpp b/image/decoders/nsBMPDecoder.cpp index 86342d8bfb9e..1bbae37fab96 100644 --- a/image/decoders/nsBMPDecoder.cpp +++ b/image/decoders/nsBMPDecoder.cpp @@ -103,6 +103,7 @@ #include "RasterImage.h" #include "SurfacePipeFactory.h" +#include "gfxPlatform.h" #include 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::ReadFileHeader( @@ -497,6 +534,43 @@ LexerTransition 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(LittleEndian::readUint32(aData + 52)) + : InfoColorSpace::SRGB; + mH.mCsIntent = aLength >= 108 ? static_cast( + 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::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::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::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::AllocateSurface() { SurfaceFormat format; SurfacePipeFlags pipeFlags = SurfacePipeFlags(); @@ -665,9 +889,13 @@ LexerTransition 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 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::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) { diff --git a/image/decoders/nsBMPDecoder.h b/image/decoders/nsBMPDecoder.h index 3a7df78b288b..c7990834b994 100644 --- a/image/decoders/nsBMPDecoder.h +++ b/image/decoders/nsBMPDecoder.h @@ -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 ReadFileHeader(const char* aData, size_t aLength); LexerTransition ReadInfoHeaderSize(const char* aData, size_t aLength); LexerTransition ReadInfoHeaderRest(const char* aData, size_t aLength); LexerTransition ReadBitfields(const char* aData, size_t aLength); + LexerTransition SeekColorProfile(size_t aLength); + LexerTransition ReadColorProfile(const char* aData, size_t aLength); + LexerTransition AllocateSurface(); LexerTransition ReadColorTable(const char* aData, size_t aLength); LexerTransition SkipGap(); LexerTransition AfterGap(); @@ -200,6 +234,9 @@ class nsBMPDecoder : public Decoder { StreamingLexer mLexer; + // Iterator to save return point. + Maybe mReturnIterator; + UniquePtr mRowBuffer; bmp::Header mH; diff --git a/image/test/reftest/bmp/bmpsuite/g/reftest.list b/image/test/reftest/bmp/bmpsuite/g/reftest.list index ac34694e41fb..098400c47ea3 100644 --- a/image/test/reftest/bmp/bmpsuite/g/reftest.list +++ b/image/test/reftest/bmp/bmpsuite/g/reftest.list @@ -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. I’m not sure that the gamma and chromaticity values in this # file are sensible, because I can’t 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 diff --git a/image/test/reftest/bmp/bmpsuite/q/reftest.list b/image/test/reftest/bmp/bmpsuite/q/reftest.list index 31aa5e3b0dbd..47bd38c0ebf0 100644 --- a/image/test/reftest/bmp/bmpsuite/q/reftest.list +++ b/image/test/reftest/bmp/bmpsuite/q/reftest.list @@ -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." diff --git a/image/test/reftest/bmp/bmpsuite/q/rgb24prof2.png b/image/test/reftest/bmp/bmpsuite/q/rgb24prof2.png deleted file mode 100644 index b65ccc529572f4e8fab91dfb7c26aa0171633476..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19650 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 A)Bpeg