/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Common.h" #include #include "nsDirectoryServiceDefs.h" #include "nsIDirectoryService.h" #include "nsIFile.h" #include "nsIInputStream.h" #include "nsIProperties.h" #include "nsNetUtil.h" #include "mozilla/RefPtr.h" #include "nsStreamUtils.h" #include "nsString.h" namespace mozilla { namespace image { using namespace gfx; using std::abs; using std::vector; /////////////////////////////////////////////////////////////////////////////// // General Helpers /////////////////////////////////////////////////////////////////////////////// // These macros work like gtest's ASSERT_* macros, except that they can be used // in functions that return values. #define ASSERT_TRUE_OR_RETURN(e, rv) \ EXPECT_TRUE(e); \ if (!(e)) { \ return rv; \ } #define ASSERT_EQ_OR_RETURN(a, b, rv) \ EXPECT_EQ(a, b); \ if ((a) != (b)) { \ return rv; \ } #define ASSERT_GE_OR_RETURN(a, b, rv) \ EXPECT_GE(a, b); \ if (!((a) >= (b))) { \ return rv; \ } #define ASSERT_LE_OR_RETURN(a, b, rv) \ EXPECT_LE(a, b); \ if (!((a) <= (b))) { \ return rv; \ } #define ASSERT_LT_OR_RETURN(a, b, rv) \ EXPECT_LT(a, b); \ if (!((a) < (b))) { \ return rv; \ } already_AddRefed LoadFile(const char* aRelativePath) { nsresult rv; nsCOMPtr dirService = do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); ASSERT_TRUE_OR_RETURN(dirService != nullptr, nullptr); // Retrieve the current working directory. nsCOMPtr file; rv = dirService->Get(NS_OS_CURRENT_WORKING_DIR, NS_GET_IID(nsIFile), getter_AddRefs(file)); ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr); // Construct the final path by appending the working path to the current // working directory. file->AppendNative(nsDependentCString(aRelativePath)); // Construct an input stream for the requested file. nsCOMPtr inputStream; rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), file); ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr); // Ensure the resulting input stream is buffered. if (!NS_InputStreamIsBuffered(inputStream)) { nsCOMPtr bufStream; rv = NS_NewBufferedInputStream(getter_AddRefs(bufStream), inputStream, 1024); ASSERT_TRUE_OR_RETURN(NS_SUCCEEDED(rv), nullptr); inputStream = bufStream; } return inputStream.forget(); } bool IsSolidColor(SourceSurface* aSurface, BGRAColor aColor, uint8_t aFuzz /* = 0 */) { IntSize size = aSurface->GetSize(); return RectIsSolidColor(aSurface, IntRect(0, 0, size.width, size.height), aColor, aFuzz); } bool IsSolidPalettedColor(Decoder* aDecoder, uint8_t aColor) { RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); return PalettedRectIsSolidColor(aDecoder, currentFrame->GetRect(), aColor); } bool RowsAreSolidColor(SourceSurface* aSurface, int32_t aStartRow, int32_t aRowCount, BGRAColor aColor, uint8_t aFuzz /* = 0 */) { IntSize size = aSurface->GetSize(); return RectIsSolidColor(aSurface, IntRect(0, aStartRow, size.width, aRowCount), aColor, aFuzz); } bool PalettedRowsAreSolidColor(Decoder* aDecoder, int32_t aStartRow, int32_t aRowCount, uint8_t aColor) { RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); IntRect frameRect = currentFrame->GetRect(); IntRect solidColorRect(frameRect.x, aStartRow, frameRect.width, aRowCount); return PalettedRectIsSolidColor(aDecoder, solidColorRect, aColor); } bool RectIsSolidColor(SourceSurface* aSurface, const IntRect& aRect, BGRAColor aColor, uint8_t aFuzz /* = 0 */) { IntSize surfaceSize = aSurface->GetSize(); IntRect rect = aRect.Intersect(IntRect(0, 0, surfaceSize.width, surfaceSize.height)); RefPtr dataSurface = aSurface->GetDataSurface(); ASSERT_TRUE_OR_RETURN(dataSurface != nullptr, false); ASSERT_EQ_OR_RETURN(dataSurface->Stride(), surfaceSize.width * 4, false); DataSourceSurface::ScopedMap mapping(dataSurface, DataSourceSurface::MapType::READ); ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false); uint8_t* data = dataSurface->GetData(); ASSERT_TRUE_OR_RETURN(data != nullptr, false); int32_t rowLength = dataSurface->Stride(); for (int32_t row = rect.y; row < rect.YMost(); ++row) { for (int32_t col = rect.x; col < rect.XMost(); ++col) { int32_t i = row * rowLength + col * 4; if (aFuzz != 0) { ASSERT_LE_OR_RETURN(abs(aColor.mBlue - data[i + 0]), aFuzz, false); ASSERT_LE_OR_RETURN(abs(aColor.mGreen - data[i + 1]), aFuzz, false); ASSERT_LE_OR_RETURN(abs(aColor.mRed - data[i + 2]), aFuzz, false); ASSERT_LE_OR_RETURN(abs(aColor.mAlpha - data[i + 3]), aFuzz, false); } else { ASSERT_EQ_OR_RETURN(aColor.mBlue, data[i + 0], false); ASSERT_EQ_OR_RETURN(aColor.mGreen, data[i + 1], false); ASSERT_EQ_OR_RETURN(aColor.mRed, data[i + 2], false); ASSERT_EQ_OR_RETURN(aColor.mAlpha, data[i + 3], false); } } } return true; } bool PalettedRectIsSolidColor(Decoder* aDecoder, const IntRect& aRect, uint8_t aColor) { RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); uint8_t* imageData; uint32_t imageLength; currentFrame->GetImageData(&imageData, &imageLength); ASSERT_TRUE_OR_RETURN(imageData, false); // Clamp to the frame rect. If any pixels outside the frame rect are included, // we immediately fail, because such pixels don't have any "color" in the // sense this function measures - they're transparent, and that doesn't // necessarily correspond to any color palette index at all. IntRect frameRect = currentFrame->GetRect(); ASSERT_EQ_OR_RETURN(imageLength, uint32_t(frameRect.Area()), false); IntRect rect = aRect.Intersect(frameRect); ASSERT_EQ_OR_RETURN(rect.Area(), aRect.Area(), false); // Translate |rect| by |frameRect.TopLeft()| to reflect the fact that the // frame rect's offset doesn't actually mean anything in terms of the // in-memory representation of the surface. The image data starts at the upper // left corner of the frame rect, in other words. rect -= frameRect.TopLeft(); // Walk through the image data and make sure that the entire rect has the // palette index |aColor|. int32_t rowLength = frameRect.width; for (int32_t row = rect.y; row < rect.YMost(); ++row) { for (int32_t col = rect.x; col < rect.XMost(); ++col) { int32_t i = row * rowLength + col; ASSERT_EQ_OR_RETURN(aColor, imageData[i], false); } } return true; } bool RowHasPixels(SourceSurface* aSurface, int32_t aRow, const vector& aPixels) { ASSERT_GE_OR_RETURN(aRow, 0, false); IntSize surfaceSize = aSurface->GetSize(); ASSERT_EQ_OR_RETURN(aPixels.size(), size_t(surfaceSize.width), false); ASSERT_LT_OR_RETURN(aRow, surfaceSize.height, false); RefPtr dataSurface = aSurface->GetDataSurface(); ASSERT_TRUE_OR_RETURN(dataSurface, false); ASSERT_EQ_OR_RETURN(dataSurface->Stride(), surfaceSize.width * 4, false); DataSourceSurface::ScopedMap mapping(dataSurface, DataSourceSurface::MapType::READ); ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false); uint8_t* data = dataSurface->GetData(); ASSERT_TRUE_OR_RETURN(data != nullptr, false); int32_t rowLength = dataSurface->Stride(); for (int32_t col = 0; col < surfaceSize.width; ++col) { int32_t i = aRow * rowLength + col * 4; ASSERT_EQ_OR_RETURN(aPixels[col].mBlue, data[i + 0], false); ASSERT_EQ_OR_RETURN(aPixels[col].mGreen, data[i + 1], false); ASSERT_EQ_OR_RETURN(aPixels[col].mRed, data[i + 2], false); ASSERT_EQ_OR_RETURN(aPixels[col].mAlpha, data[i + 3], false); } return true; } /////////////////////////////////////////////////////////////////////////////// // SurfacePipe Helpers /////////////////////////////////////////////////////////////////////////////// already_AddRefed CreateTrivialDecoder() { gfxPrefs::GetSingleton(); DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif"); NotNull> sourceBuffer = WrapNotNull(new SourceBuffer()); RefPtr decoder = DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(), DefaultSurfaceFlags()); return decoder.forget(); } void AssertCorrectPipelineFinalState(SurfaceFilter* aFilter, const gfx::IntRect& aInputSpaceRect, const gfx::IntRect& aOutputSpaceRect) { EXPECT_TRUE(aFilter->IsSurfaceFinished()); Maybe invalidRect = aFilter->TakeInvalidRect(); EXPECT_TRUE(invalidRect.isSome()); EXPECT_EQ(aInputSpaceRect, invalidRect->mInputSpaceRect); EXPECT_EQ(aOutputSpaceRect, invalidRect->mOutputSpaceRect); } void CheckGeneratedImage(Decoder* aDecoder, const IntRect& aRect, uint8_t aFuzz /* = 0 */) { RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); RefPtr surface = currentFrame->GetSourceSurface(); const IntSize surfaceSize = surface->GetSize(); // This diagram shows how the surface is divided into regions that the code // below tests for the correct content. The output rect is the bounds of the // region labeled 'C'. // // +---------------------------+ // | A | // +---------+--------+--------+ // | B | C | D | // +---------+--------+--------+ // | E | // +---------------------------+ // Check that the output rect itself is green. (Region 'C'.) EXPECT_TRUE(RectIsSolidColor(surface, aRect, BGRAColor::Green(), aFuzz)); // Check that the area above the output rect is transparent. (Region 'A'.) EXPECT_TRUE(RectIsSolidColor(surface, IntRect(0, 0, surfaceSize.width, aRect.y), BGRAColor::Transparent(), aFuzz)); // Check that the area to the left of the output rect is transparent. (Region 'B'.) EXPECT_TRUE(RectIsSolidColor(surface, IntRect(0, aRect.y, aRect.x, aRect.YMost()), BGRAColor::Transparent(), aFuzz)); // Check that the area to the right of the output rect is transparent. (Region 'D'.) const int32_t widthOnRight = surfaceSize.width - aRect.XMost(); EXPECT_TRUE(RectIsSolidColor(surface, IntRect(aRect.XMost(), aRect.y, widthOnRight, aRect.YMost()), BGRAColor::Transparent(), aFuzz)); // Check that the area below the output rect is transparent. (Region 'E'.) const int32_t heightBelow = surfaceSize.height - aRect.YMost(); EXPECT_TRUE(RectIsSolidColor(surface, IntRect(0, aRect.YMost(), surfaceSize.width, heightBelow), BGRAColor::Transparent(), aFuzz)); } void CheckGeneratedPalettedImage(Decoder* aDecoder, const IntRect& aRect) { RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); IntSize imageSize = currentFrame->GetImageSize(); // This diagram shows how the surface is divided into regions that the code // below tests for the correct content. The output rect is the bounds of the // region labeled 'C'. // // +---------------------------+ // | A | // +---------+--------+--------+ // | B | C | D | // +---------+--------+--------+ // | E | // +---------------------------+ // Check that the output rect itself is all 255's. (Region 'C'.) EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder, aRect, 255)); // Check that the area above the output rect is all 0's. (Region 'A'.) EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder, IntRect(0, 0, imageSize.width, aRect.y), 0)); // Check that the area to the left of the output rect is all 0's. (Region 'B'.) EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder, IntRect(0, aRect.y, aRect.x, aRect.YMost()), 0)); // Check that the area to the right of the output rect is all 0's. (Region 'D'.) const int32_t widthOnRight = imageSize.width - aRect.XMost(); EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder, IntRect(aRect.XMost(), aRect.y, widthOnRight, aRect.YMost()), 0)); // Check that the area below the output rect is transparent. (Region 'E'.) const int32_t heightBelow = imageSize.height - aRect.YMost(); EXPECT_TRUE(PalettedRectIsSolidColor(aDecoder, IntRect(0, aRect.YMost(), imageSize.width, heightBelow), 0)); } void CheckWritePixels(Decoder* aDecoder, SurfaceFilter* aFilter, Maybe aOutputRect /* = Nothing() */, Maybe aInputRect /* = Nothing() */, Maybe aInputWriteRect /* = Nothing() */, Maybe aOutputWriteRect /* = Nothing() */, uint8_t aFuzz /* = 0 */) { IntRect outputRect = aOutputRect.valueOr(IntRect(0, 0, 100, 100)); IntRect inputRect = aInputRect.valueOr(IntRect(0, 0, 100, 100)); IntRect inputWriteRect = aInputWriteRect.valueOr(inputRect); IntRect outputWriteRect = aOutputWriteRect.valueOr(outputRect); // Fill the image. int32_t count = 0; auto result = aFilter->WritePixels([&] { ++count; return AsVariant(BGRAColor::Green().AsPixel()); }); EXPECT_EQ(WriteState::FINISHED, result); EXPECT_EQ(inputWriteRect.width * inputWriteRect.height, count); AssertCorrectPipelineFinalState(aFilter, inputRect, outputRect); // Attempt to write more data and make sure nothing changes. const int32_t oldCount = count; result = aFilter->WritePixels([&] { ++count; return AsVariant(BGRAColor::Green().AsPixel()); }); EXPECT_EQ(oldCount, count); EXPECT_EQ(WriteState::FINISHED, result); EXPECT_TRUE(aFilter->IsSurfaceFinished()); Maybe invalidRect = aFilter->TakeInvalidRect(); EXPECT_TRUE(invalidRect.isNothing()); // Attempt to advance to the next row and make sure nothing changes. aFilter->AdvanceRow(); EXPECT_TRUE(aFilter->IsSurfaceFinished()); invalidRect = aFilter->TakeInvalidRect(); EXPECT_TRUE(invalidRect.isNothing()); // Check that the generated image is correct. CheckGeneratedImage(aDecoder, outputWriteRect, aFuzz); } void CheckPalettedWritePixels(Decoder* aDecoder, SurfaceFilter* aFilter, Maybe aOutputRect /* = Nothing() */, Maybe aInputRect /* = Nothing() */, Maybe aInputWriteRect /* = Nothing() */, Maybe aOutputWriteRect /* = Nothing() */, uint8_t aFuzz /* = 0 */) { IntRect outputRect = aOutputRect.valueOr(IntRect(0, 0, 100, 100)); IntRect inputRect = aInputRect.valueOr(IntRect(0, 0, 100, 100)); IntRect inputWriteRect = aInputWriteRect.valueOr(inputRect); IntRect outputWriteRect = aOutputWriteRect.valueOr(outputRect); // Fill the image. int32_t count = 0; auto result = aFilter->WritePixels([&] { ++count; return AsVariant(uint8_t(255)); }); EXPECT_EQ(WriteState::FINISHED, result); EXPECT_EQ(inputWriteRect.width * inputWriteRect.height, count); AssertCorrectPipelineFinalState(aFilter, inputRect, outputRect); // Attempt to write more data and make sure nothing changes. const int32_t oldCount = count; result = aFilter->WritePixels([&] { ++count; return AsVariant(uint8_t(255)); }); EXPECT_EQ(oldCount, count); EXPECT_EQ(WriteState::FINISHED, result); EXPECT_TRUE(aFilter->IsSurfaceFinished()); Maybe invalidRect = aFilter->TakeInvalidRect(); EXPECT_TRUE(invalidRect.isNothing()); // Attempt to advance to the next row and make sure nothing changes. aFilter->AdvanceRow(); EXPECT_TRUE(aFilter->IsSurfaceFinished()); invalidRect = aFilter->TakeInvalidRect(); EXPECT_TRUE(invalidRect.isNothing()); // Check that the generated image is correct. RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); uint8_t* imageData; uint32_t imageLength; currentFrame->GetImageData(&imageData, &imageLength); ASSERT_TRUE(imageData != nullptr); ASSERT_EQ(outputWriteRect.width * outputWriteRect.height, int32_t(imageLength)); for (uint32_t i = 0; i < imageLength; ++i) { ASSERT_EQ(uint8_t(255), imageData[i]); } } /////////////////////////////////////////////////////////////////////////////// // Test Data /////////////////////////////////////////////////////////////////////////////// ImageTestCase GreenPNGTestCase() { return ImageTestCase("green.png", "image/png", IntSize(100, 100)); } ImageTestCase GreenGIFTestCase() { return ImageTestCase("green.gif", "image/gif", IntSize(100, 100)); } ImageTestCase GreenJPGTestCase() { return ImageTestCase("green.jpg", "image/jpeg", IntSize(100, 100), TEST_CASE_IS_FUZZY); } ImageTestCase GreenBMPTestCase() { return ImageTestCase("green.bmp", "image/bmp", IntSize(100, 100)); } ImageTestCase GreenICOTestCase() { // This ICO contains a 32-bit BMP, and we use a BMP's alpha data by default // when the BMP is embedded in an ICO, so it's transparent. return ImageTestCase("green.ico", "image/x-icon", IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); } ImageTestCase GreenIconTestCase() { return ImageTestCase("green.icon", "image/icon", IntSize(100, 100), TEST_CASE_IS_TRANSPARENT); } ImageTestCase GreenFirstFrameAnimatedGIFTestCase() { return ImageTestCase("first-frame-green.gif", "image/gif", IntSize(100, 100), TEST_CASE_IS_ANIMATED); } ImageTestCase GreenFirstFrameAnimatedPNGTestCase() { return ImageTestCase("first-frame-green.png", "image/png", IntSize(100, 100), TEST_CASE_IS_TRANSPARENT | TEST_CASE_IS_ANIMATED); } ImageTestCase CorruptTestCase() { return ImageTestCase("corrupt.jpg", "image/jpeg", IntSize(100, 100), TEST_CASE_HAS_ERROR); } ImageTestCase CorruptBMPWithTruncatedHeader() { // This BMP has a header which is truncated right between the BIH and the // bitfields, which is a particularly error-prone place w.r.t. the BMP decoder // state machine. return ImageTestCase("invalid-truncated-metadata.bmp", "image/bmp", IntSize(100, 100), TEST_CASE_HAS_ERROR); } ImageTestCase CorruptICOWithBadBMPWidthTestCase() { // This ICO contains a BMP icon which has a width that doesn't match the size // listed in the corresponding ICO directory entry. return ImageTestCase("corrupt-with-bad-bmp-width.ico", "image/x-icon", IntSize(100, 100), TEST_CASE_HAS_ERROR); } ImageTestCase CorruptICOWithBadBMPHeightTestCase() { // This ICO contains a BMP icon which has a height that doesn't match the size // listed in the corresponding ICO directory entry. return ImageTestCase("corrupt-with-bad-bmp-height.ico", "image/x-icon", IntSize(100, 100), TEST_CASE_HAS_ERROR); } ImageTestCase TransparentPNGTestCase() { return ImageTestCase("transparent.png", "image/png", IntSize(32, 32), TEST_CASE_IS_TRANSPARENT); } ImageTestCase TransparentGIFTestCase() { return ImageTestCase("transparent.gif", "image/gif", IntSize(16, 16), TEST_CASE_IS_TRANSPARENT); } ImageTestCase FirstFramePaddingGIFTestCase() { return ImageTestCase("transparent.gif", "image/gif", IntSize(16, 16), TEST_CASE_IS_TRANSPARENT); } ImageTestCase TransparentIfWithinICOBMPTestCase(TestCaseFlags aFlags) { // This is a BMP that is only transparent when decoded as if it is within an // ICO file. (Note: aFlags needs to be set to TEST_CASE_DEFAULT_FLAGS or // TEST_CASE_IS_TRANSPARENT accordingly.) return ImageTestCase("transparent-if-within-ico.bmp", "image/bmp", IntSize(32, 32), aFlags); } ImageTestCase RLE4BMPTestCase() { return ImageTestCase("rle4.bmp", "image/bmp", IntSize(320, 240), TEST_CASE_IS_TRANSPARENT); } ImageTestCase RLE8BMPTestCase() { return ImageTestCase("rle8.bmp", "image/bmp", IntSize(32, 32), TEST_CASE_IS_TRANSPARENT); } ImageTestCase NoFrameDelayGIFTestCase() { // This is an invalid (or at least, questionably valid) GIF that's animated // even though it specifies a frame delay of zero. It's animated, but it's not // marked TEST_CASE_IS_ANIMATED because the metadata decoder can't detect that // it's animated. return ImageTestCase("no-frame-delay.gif", "image/gif", IntSize(100, 100)); } ImageTestCase ExtraImageSubBlocksAnimatedGIFTestCase() { // This is a corrupt GIF that has extra image sub blocks between the first and // second frame. return ImageTestCase("animated-with-extra-image-sub-blocks.gif", "image/gif", IntSize(100, 100)); } ImageTestCase DownscaledPNGTestCase() { // This testcase (and all the other "downscaled") testcases) consists of 25 // lines of green, followed by 25 lines of red, followed by 25 lines of green, // followed by 25 more lines of red. It's intended that tests downscale it // from 100x100 to 20x20, so we specify a 20x20 output size. return ImageTestCase("downscaled.png", "image/png", IntSize(100, 100), IntSize(20, 20)); } ImageTestCase DownscaledGIFTestCase() { return ImageTestCase("downscaled.gif", "image/gif", IntSize(100, 100), IntSize(20, 20)); } ImageTestCase DownscaledJPGTestCase() { return ImageTestCase("downscaled.jpg", "image/jpeg", IntSize(100, 100), IntSize(20, 20)); } ImageTestCase DownscaledBMPTestCase() { return ImageTestCase("downscaled.bmp", "image/bmp", IntSize(100, 100), IntSize(20, 20)); } ImageTestCase DownscaledICOTestCase() { return ImageTestCase("downscaled.ico", "image/x-icon", IntSize(100, 100), IntSize(20, 20), TEST_CASE_IS_TRANSPARENT); } ImageTestCase DownscaledIconTestCase() { return ImageTestCase("downscaled.icon", "image/icon", IntSize(100, 100), IntSize(20, 20), TEST_CASE_IS_TRANSPARENT); } ImageTestCase DownscaledTransparentICOWithANDMaskTestCase() { // This test case is an ICO with AND mask transparency. We want to ensure that // we can downscale it without crashing or triggering ASAN failures, but its // content isn't simple to verify, so for now we don't check the output. return ImageTestCase("transparent-ico-with-and-mask.ico", "image/x-icon", IntSize(32, 32), IntSize(20, 20), TEST_CASE_IS_TRANSPARENT | TEST_CASE_IGNORE_OUTPUT); } ImageTestCase TruncatedSmallGIFTestCase() { return ImageTestCase("green-1x1-truncated.gif", "image/gif", IntSize(1, 1)); } } // namespace image } // namespace mozilla