#include "gtest/gtest.h" #include #include #include #include #include "ImageContainer.h" #include "YCbCrUtils.h" #include "nsTArray.h" using Color = std::tuple; using namespace mozilla; const Color BLACK(0, 0, 0); const Color BLUE(0, 0, 255); const Color GREEN(0, 255, 0); const Color CYAN(0, 255, 255); const Color RED(255, 0, 0); const Color MAGENTA(255, 0, 255); const Color YELLOW(255, 255, 0); const Color WHITE(255, 255, 255); const Color CHOCOLATE(210, 105, 30); const Color PERU(205, 133, 63); const Color ROSYBROWN(188, 143, 143); const Color STEELBLUE(70, 130, 180); const std::array COLOR_LIST = { BLACK, BLUE, GREEN, CYAN, RED, MAGENTA, YELLOW, WHITE, CHOCOLATE, PERU, ROSYBROWN, STEELBLUE}; Color RGB2YUV(const Color& aRGBColor) { const uint8_t& r = std::get<0>(aRGBColor); const uint8_t& g = std::get<1>(aRGBColor); const uint8_t& b = std::get<2>(aRGBColor); const double y = r * 0.299 + g * 0.587 + b * 0.114; const double u = r * -0.168736 + g * -0.331264 + b * 0.5 + 128; const double v = r * 0.5 + g * -0.418688 + b * -0.081312 + 128; return Color(round(y), round(u), round(v)); } int32_t CeilingOfHalf(int32_t aValue) { MOZ_ASSERT(aValue >= 0); return aValue / 2 + (aValue % 2); } already_AddRefed CreateI420Image( const Color& aRGBColor, const gfx::YUVColorSpace& aColorSpace, const gfx::IntSize& aSize, Maybe aAlphaValue = Nothing()) { const int32_t halfWidth = CeilingOfHalf(aSize.width); const int32_t halfHeight = CeilingOfHalf(aSize.height); const size_t yPlaneSize = aSize.width * aSize.height; const size_t uPlaneSize = halfWidth * halfHeight; const size_t vPlaneSize = uPlaneSize; const size_t aPlaneSize = aAlphaValue.isSome() ? yPlaneSize : 0; const size_t imageSize = yPlaneSize + uPlaneSize + vPlaneSize + aPlaneSize; const Color yuvColor = RGB2YUV(aRGBColor); const uint8_t& yColor = std::get<0>(yuvColor); const uint8_t& uColor = std::get<1>(yuvColor); const uint8_t& vColor = std::get<2>(yuvColor); UniquePtr buffer(new uint8_t[imageSize]); layers::PlanarYCbCrData data; data.mPictureRect = gfx::IntRect({0, 0}, aSize); // Y plane. uint8_t* yChannel = buffer.get(); memset(yChannel, yColor, yPlaneSize); data.mYChannel = yChannel; data.mYStride = aSize.width; data.mYSkip = 0; // Cb plane (aka U). uint8_t* uChannel = yChannel + yPlaneSize; memset(uChannel, uColor, uPlaneSize); data.mCbChannel = uChannel; data.mCbSkip = 0; // Cr plane (aka V). uint8_t* vChannel = uChannel + uPlaneSize; memset(vChannel, vColor, vPlaneSize); data.mCrChannel = vChannel; data.mCrSkip = 0; // CrCb plane vectors. data.mCbCrStride = halfWidth; data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; // Alpha plane. if (aPlaneSize) { uint8_t* aChannel = vChannel + vPlaneSize; memset(aChannel, *aAlphaValue, aPlaneSize); data.mAlpha.emplace(); data.mAlpha->mChannel = aChannel; data.mAlpha->mSize = aSize; } data.mYUVColorSpace = aColorSpace; RefPtr image = new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin()); image->CopyData(data); return image.forget(); } already_AddRefed CreateI444Image( const Color& aRGBColor, const gfx::YUVColorSpace& aColorSpace, const gfx::IntSize& aSize, Maybe aAlphaValue = Nothing()) { const size_t yPlaneSize = aSize.width * aSize.height; const size_t uPlaneSize = yPlaneSize; const size_t vPlaneSize = yPlaneSize; const size_t aPlaneSize = aAlphaValue.isSome() ? yPlaneSize : 0; const size_t imageSize = yPlaneSize + uPlaneSize + vPlaneSize + aPlaneSize; const Color yuvColor = RGB2YUV(aRGBColor); const uint8_t& yColor = std::get<0>(yuvColor); const uint8_t& uColor = std::get<1>(yuvColor); const uint8_t& vColor = std::get<2>(yuvColor); UniquePtr buffer(new uint8_t[imageSize]); layers::PlanarYCbCrData data; data.mPictureRect = gfx::IntRect({0, 0}, aSize); // Y plane. uint8_t* yChannel = buffer.get(); memset(yChannel, yColor, yPlaneSize); data.mYChannel = yChannel; data.mYStride = aSize.width; data.mYSkip = 0; // Cb plane (aka U). uint8_t* uChannel = yChannel + yPlaneSize; memset(uChannel, uColor, uPlaneSize); data.mCbChannel = uChannel; data.mCbSkip = 0; // Cr plane (aka V). uint8_t* vChannel = uChannel + uPlaneSize; memset(vChannel, vColor, vPlaneSize); data.mCrChannel = vChannel; data.mCrSkip = 0; // CrCb plane vectors. data.mCbCrStride = data.mYStride; data.mChromaSubsampling = gfx::ChromaSubsampling::FULL; // Alpha plane. if (aPlaneSize) { uint8_t* aChannel = vChannel + vPlaneSize; memset(aChannel, *aAlphaValue, aPlaneSize); data.mAlpha.emplace(); data.mAlpha->mChannel = aChannel; data.mAlpha->mSize = aSize; } data.mYUVColorSpace = aColorSpace; RefPtr image = new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin()); image->CopyData(data); return image.forget(); } void IsColorEqual(uint8_t* aBGRX, uint8_t* aRGBX, size_t aSize) { ASSERT_EQ(aSize % 4, (size_t)0); for (size_t i = 0; i < aSize; i += 4) { ASSERT_EQ(aBGRX[i + 2], aRGBX[i]); // R ASSERT_EQ(aBGRX[i + 1], aRGBX[i + 1]); // G ASSERT_EQ(aBGRX[i], aRGBX[i + 2]); // B ASSERT_EQ(aBGRX[i + 3], aRGBX[i + 3]); // X or A } } uint32_t Hash(const Color& aColor) { const uint8_t& r = std::get<0>(aColor); const uint8_t& g = std::get<1>(aColor); const uint8_t& b = std::get<2>(aColor); return r << 16 | g << 8 | b; } std::unordered_map> GetExpectedConvertedRGB() { static std::unordered_map> map; map.emplace(Hash(BLACK), std::array{// gfx::YUVColorSpace::BT601 Color(0, 0, 0), // gfx::YUVColorSpace::BT709 Color(0, 0, 0), // gfx::YUVColorSpace::BT2020 Color(0, 0, 0)}); map.emplace(Hash(BLUE), std::array{// gfx::YUVColorSpace::BT601 Color(0, 82, 0), // gfx::YUVColorSpace::BT709 Color(0, 54, 0), // gfx::YUVColorSpace::BT2020 Color(0, 53, 0)}); map.emplace(Hash(GREEN), std::array{// gfx::YUVColorSpace::BT601 Color(0, 255, 0), // gfx::YUVColorSpace::BT709 Color(0, 231, 0), // gfx::YUVColorSpace::BT2020 Color(0, 242, 0)}); map.emplace(Hash(CYAN), std::array{// gfx::YUVColorSpace::BT601 Color(0, 255, 255), // gfx::YUVColorSpace::BT709 Color(0, 248, 255), // gfx::YUVColorSpace::BT2020 Color(0, 255, 255)}); map.emplace(Hash(RED), std::array{// gfx::YUVColorSpace::BT601 Color(0, 191, 0), // gfx::YUVColorSpace::BT709 Color(0, 147, 0), // gfx::YUVColorSpace::BT2020 Color(0, 162, 0)}); map.emplace(Hash(MAGENTA), std::array{// gfx::YUVColorSpace::BT601 Color(255, 0, 255), // gfx::YUVColorSpace::BT709 Color(255, 28, 255), // gfx::YUVColorSpace::BT2020 Color(255, 18, 255)}); map.emplace(Hash(YELLOW), std::array{// gfx::YUVColorSpace::BT601 Color(255, 255, 0), // gfx::YUVColorSpace::BT709 Color(255, 255, 0), // gfx::YUVColorSpace::BT2020 Color(255, 255, 0)}); map.emplace(Hash(WHITE), std::array{// gfx::YUVColorSpace::BT601 Color(255, 255, 255), // gfx::YUVColorSpace::BT709 Color(255, 255, 255), // gfx::YUVColorSpace::BT2020 Color(255, 255, 255)}); map.emplace(Hash(CHOCOLATE), std::array{// gfx::YUVColorSpace::BT601 Color(224, 104, 20), // gfx::YUVColorSpace::BT709 Color(236, 111, 20), // gfx::YUVColorSpace::BT2020 Color(229, 102, 20)}); map.emplace(Hash(PERU), std::array{// gfx::YUVColorSpace::BT601 Color(219, 137, 58), // gfx::YUVColorSpace::BT709 Color(228, 140, 58), // gfx::YUVColorSpace::BT2020 Color(223, 134, 59)}); map.emplace(Hash(ROSYBROWN), std::array{// gfx::YUVColorSpace::BT601 Color(200, 147, 147), // gfx::YUVColorSpace::BT709 Color(204, 152, 147), // gfx::YUVColorSpace::BT2020 Color(201, 149, 147)}); map.emplace(Hash(STEELBLUE), std::array{// gfx::YUVColorSpace::BT601 Color(65, 133, 189), // gfx::YUVColorSpace::BT709 Color(58, 129, 189), // gfx::YUVColorSpace::BT2020 Color(62, 135, 189)}); return map; } void IsColorMatched(const Color& aColor, uint8_t* aRGBX, size_t aSize, Maybe aAlphaValue = Nothing()) { const uint8_t& r = std::get<0>(aColor); const uint8_t& g = std::get<1>(aColor); const uint8_t& b = std::get<2>(aColor); for (size_t i = 0; i < aSize; i += 4) { ASSERT_EQ(r, aRGBX[i]); // R ASSERT_EQ(g, aRGBX[i + 1]); // G ASSERT_EQ(b, aRGBX[i + 2]); // B if (aAlphaValue) { ASSERT_EQ(*aAlphaValue, aRGBX[i + 3]); // A } } } TEST(YCbCrUtils, ConvertYCbCrToRGB32) { const gfx::IntSize imgSize(32, 16); const int32_t stride = imgSize.Width() * gfx::BytesPerPixel(gfx::SurfaceFormat::B8G8R8X8); const size_t bufferSize = stride * imgSize.Height(); const std::array colorSpaces{ gfx::YUVColorSpace::BT601, gfx::YUVColorSpace::BT709, gfx::YUVColorSpace::BT2020}; std::unordered_map> expectations = GetExpectedConvertedRGB(); for (const Color& color : COLOR_LIST) { const std::array& expectedColors = expectations[Hash(color)]; for (const gfx::YUVColorSpace& colorSpace : colorSpaces) { RefPtr img = CreateI420Image(color, colorSpace, imgSize); UniquePtr BGRX = MakeUnique(bufferSize); ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::B8G8R8X8, BGRX.get(), stride, nullptr); UniquePtr RGBX = MakeUnique(bufferSize); ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::R8G8B8X8, RGBX.get(), stride, nullptr); IsColorEqual(BGRX.get(), RGBX.get(), bufferSize); Color expectation = expectedColors[static_cast(colorSpace)]; IsColorMatched(expectation, RGBX.get(), bufferSize); } } } TEST(YCbCrUtils, ConvertYCbCrToRGB32WithAlpha) { const gfx::IntSize imgSize(32, 16); const int32_t stride = imgSize.Width() * gfx::BytesPerPixel(gfx::SurfaceFormat::B8G8R8A8); const size_t bufferSize = stride * imgSize.Height(); const std::array colorSpaces{ gfx::YUVColorSpace::BT601, gfx::YUVColorSpace::BT709, gfx::YUVColorSpace::BT2020}; std::unordered_map> expectations = GetExpectedConvertedRGB(); for (const Color& color : COLOR_LIST) { const std::array& expectedColors = expectations[Hash(color)]; for (const gfx::YUVColorSpace& colorSpace : colorSpaces) { Maybe alpha = Some(128); RefPtr img = CreateI420Image(color, colorSpace, imgSize, alpha); UniquePtr BGRA = MakeUnique(bufferSize); ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::B8G8R8A8, BGRA.get(), stride, nullptr); UniquePtr RGBA = MakeUnique(bufferSize); ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::R8G8B8A8, RGBA.get(), stride, nullptr); IsColorEqual(BGRA.get(), RGBA.get(), bufferSize); Color expectation = expectedColors[static_cast(colorSpace)]; IsColorMatched(expectation, RGBA.get(), bufferSize, alpha); } } } TEST(YCbCrUtils, ConvertYCbCrToRGB32WithIdentityColorSpace) { const gfx::IntSize imgSize(32, 16); const int32_t stride = imgSize.Width() * gfx::BytesPerPixel(gfx::SurfaceFormat::B8G8R8X8); const size_t bufferSize = stride * imgSize.Height(); for (const Color& color : COLOR_LIST) { RefPtr img = CreateI444Image(color, gfx::YUVColorSpace::Identity, imgSize); UniquePtr BGRX = MakeUnique(bufferSize); ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::B8G8R8X8, BGRX.get(), stride, nullptr); UniquePtr RGBX = MakeUnique(bufferSize); ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::R8G8B8X8, RGBX.get(), stride, nullptr); IsColorEqual(BGRX.get(), RGBX.get(), bufferSize); const Color yuvColor = RGB2YUV(color); const uint8_t& y = std::get<0>(yuvColor); const uint8_t& u = std::get<1>(yuvColor); const uint8_t& v = std::get<2>(yuvColor); const Color expectation(v, y, u); IsColorMatched(expectation, RGBX.get(), bufferSize); } } // Fills a 4×4 Y plane and chroma planes for a frame whose luma is divided // into four 2×2 blocks. aColors[blockRow][blockCol] gives the color for each // block. Chroma plane dimensions depend on aSubsampling: // FULL (YV24): 4×4 chroma, each pixel maps 1:1 to a luma pixel. // HALF_WIDTH (YV16): 2×4 chroma, half-width but full height. // HALF_WIDTH_AND_HEIGHT (YV12): 2×2 chroma. // Callers must provide at least 16 bytes for aUBuf/aVBuf to cover the FULL // case; smaller subsamplings use only a prefix of that. static void FillTwoByTwoFrame(const Color aColors[2][2], gfx::ChromaSubsampling aSubsampling, uint8_t* aYBuf, uint8_t* aUBuf, uint8_t* aVBuf) { // Give each luma pixel a unique Y by adding a small per-pixel offset based // on its position within its 2x2 chroma block: +0/+2/+4/+6 for // (top-left/top-right/bottom-left/bottom-right). This makes luma sampling // bugs detectable without meaningfully shifting the color. for (int r = 0; r < 4; r++) { for (int c = 0; c < 4; c++) { uint8_t baseY = std::get<0>(RGB2YUV(aColors[r / 2][c / 2])); aYBuf[r * 4 + c] = baseY + (r % 2) * 4 + (c % 2) * 2; } } int chromaWidth = (aSubsampling == gfx::ChromaSubsampling::FULL) ? 4 : 2; int chromaHeight = (aSubsampling == gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT) ? 2 : 4; for (int chromaRow = 0; chromaRow < chromaHeight; chromaRow++) { int blockRow = chromaRow * 2 / chromaHeight; for (int chromaCol = 0; chromaCol < chromaWidth; chromaCol++) { int blockCol = chromaCol * 2 / chromaWidth; aUBuf[chromaRow * chromaWidth + chromaCol] = std::get<1>(RGB2YUV(aColors[blockRow][blockCol])); aVBuf[chromaRow * chromaWidth + chromaCol] = std::get<2>(RGB2YUV(aColors[blockRow][blockCol])); } } } // Fills and converts a 4×4 test frame, writing the result into aOutput. // aStride is in bytes. See FillTwoByTwoFrame for the aColors layout. static void ConvertTestFrame(const Color aColors[2][2], gfx::ChromaSubsampling aSubsampling, const gfx::IntRect& aPictureRect, uint8_t* aOutput, int32_t aStride) { uint8_t yBuf[16], uBuf[16], vBuf[16]; FillTwoByTwoFrame(aColors, aSubsampling, yBuf, uBuf, vBuf); layers::PlanarYCbCrData data; data.mYChannel = yBuf; data.mYStride = 4; data.mYSkip = 0; data.mCbChannel = uBuf; data.mCrChannel = vBuf; data.mCbCrStride = (aSubsampling == gfx::ChromaSubsampling::FULL) ? 4 : 2; data.mCbSkip = 0; data.mCrSkip = 0; data.mChromaSubsampling = aSubsampling; data.mYUVColorSpace = gfx::YUVColorSpace::BT709; data.mColorRange = gfx::ColorRange::LIMITED; data.mPictureRect = aPictureRect; ConvertYCbCrToRGB32(data, gfx::SurfaceFormat::R8G8B8X8, aOutput, aStride, nullptr); } // Tests for odd pic_x / pic_y offsets in YV12, YV16, and YV24. // // The 4x4 frame has four 2x2 chroma blocks with distinct mid-range colors. // Within each block each luma pixel has a unique Y value (offset +0/+2/+4/+6), // so both chroma and luma misalignment are detectable. The reference is a full // even-aligned 4x4 conversion; each odd-crop output pixel is checked against // its corresponding source position in that reference. static const gfx::ChromaSubsampling kTestSubsamplings[] = { gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT, gfx::ChromaSubsampling::HALF_WIDTH, gfx::ChromaSubsampling::FULL}; static void RunOddPicTest(const Color aColors[2][2], const gfx::IntRect& aRect) { const int32_t stride = aRect.Width() * 4; UniquePtr output = MakeUnique(aRect.Height() * stride); auto exp = GetExpectedConvertedRGB(); const size_t bt709 = static_cast(gfx::YUVColorSpace::BT709); for (gfx::ChromaSubsampling subsampling : kTestSubsamplings) { // fullRef: even-aligned 4x4 reference (no odd-offset ambiguity). uint8_t fullRef[4 * 4 * 4]; ConvertTestFrame(aColors, subsampling, gfx::IntRect(0, 0, 4, 4), fullRef, 4 * 4); // Sanity-check the reference: top-left of each 2x2 block (Y offset 0) // must match GetExpectedConvertedRGB; the other three pixels in the block // must be distinct but close (Y delta ≤6 → channel delta ~2-7). for (int br = 0; br < 2; br++) { for (int bc = 0; bc < 2; bc++) { uint8_t* base = fullRef + (br * 2) * 4 * 4 + (bc * 2) * 4; IsColorMatched(exp[Hash(aColors[br][bc])][bt709], base, 4); for (int dr = 0; dr < 2; dr++) { for (int dc = 0; dc < 2; dc++) { if (dr == 0 && dc == 0) continue; uint8_t* other = fullRef + (br * 2 + dr) * 4 * 4 + (bc * 2 + dc) * 4; for (int ch = 0; ch < 3; ch++) { ASSERT_NE(base[ch], other[ch]); ASSERT_NEAR(base[ch], other[ch], 10); } } } } } // output: the odd-crop conversion under test. ConvertTestFrame(aColors, subsampling, aRect, output.get(), stride); // Each output pixel must match its source position in the full reference. for (int row = 0; row < aRect.Height(); row++) { for (int col = 0; col < aRect.Width(); col++) { uint8_t* ref = fullRef + (aRect.y + row) * 4 * 4 + (aRect.x + col) * 4; Color expected(ref[0], ref[1], ref[2]); IsColorMatched(expected, output.get() + row * stride + col * 4, 4); } } } } TEST(YCbCrUtils, ConvertYCbCrToRGB32OddPicOffset) { const Color colors[2][2] = {{CHOCOLATE, PERU}, {ROSYBROWN, STEELBLUE}}; RunOddPicTest(colors, gfx::IntRect(1, 1, 3, 3)); // both odd RunOddPicTest(colors, gfx::IntRect(1, 0, 3, 4)); // odd pic_x only RunOddPicTest(colors, gfx::IntRect(0, 1, 4, 3)); // odd pic_y only }