/* * Copyright 2020 Google LLC * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "include/core/SkTypes.h" #ifdef SK_ENABLE_NDK_IMAGES #include "include/core/SkColorSpace.h" #include "include/ports/SkImageGeneratorNDK.h" #include "tests/Test.h" #include "tools/Resources.h" #include "tools/ToolUtils.h" #include static std::unique_ptr make_generator(const char* path, skiatest::Reporter* r) { auto data = GetResourceAsData(path); if (data) { auto gen = SkImageGeneratorNDK::MakeFromEncodedNDK(std::move(data)); if (gen) { return gen; } ERRORF(r, "Failed to create NDK generator from %s\n", path); } else { // Silently fail so developers can skip using --resources } return nullptr; } DEF_TEST(NdkDecode, r) { static const struct { const char* fPath; SkISize fSize; } recs[] = { {"images/CMYK.jpg", {642, 516}}, {"images/arrow.png", {187, 312}}, {"images/baby_tux.webp", {386, 395}}, {"images/color_wheel.gif", {128, 128}}, {"images/rle.bmp", {320, 240}}, {"images/color_wheel.ico", {128, 128}}, {"images/google_chrome.ico", {256, 256}}, {"images/mandrill.wbmp", {512, 512}}, }; for (auto& rec : recs) { auto gen = make_generator(rec.fPath, r); if (!gen) continue; const auto& info = gen->getInfo(); REPORTER_ASSERT(r, info.dimensions() == rec.fSize); SkBitmap bm; bm.allocPixels(info); REPORTER_ASSERT(r, gen->getPixels(bm.pixmap())); REPORTER_ASSERT(r, info.alphaType() != kUnpremul_SkAlphaType); auto unpremulInfo = info.makeAlphaType(kUnpremul_SkAlphaType); bm.allocPixels(unpremulInfo); REPORTER_ASSERT(r, gen->getPixels(bm.pixmap())); } } DEF_TEST(NdkDecode_nullData, r) { auto gen = SkImageGeneratorNDK::MakeFromEncodedNDK(nullptr); REPORTER_ASSERT(r, !gen); } static constexpr skcms_TransferFunction k2Dot6 = {2.6f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; static constexpr skcms_Matrix3x3 kDCIP3 = {{ {0.486143, 0.323835, 0.154234}, {0.226676, 0.710327, 0.0629966}, {0.000800549, 0.0432385, 0.78275}, }}; DEF_TEST(NdkDecode_reportedColorSpace, r) { for (sk_sp cs : { sk_sp(nullptr), SkColorSpace::MakeSRGB(), SkColorSpace::MakeSRGBLinear(), SkColorSpace::MakeRGB(SkNamedTransferFn::kRec2020, SkNamedGamut::kSRGB), SkColorSpace::MakeRGB(SkNamedTransferFn::kRec2020, SkNamedGamut::kRec2020), SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDisplayP3), SkColorSpace::MakeRGB(SkNamedTransferFn::k2Dot2, SkNamedGamut::kAdobeRGB), SkColorSpace::MakeRGB(k2Dot6, kDCIP3), }) { SkBitmap bm; bm.allocPixels(SkImageInfo::Make(10, 10, kRGBA_F16_SkColorType, kOpaque_SkAlphaType, cs)); bm.eraseColor(SK_ColorBLUE); for (auto format : { SkEncodedImageFormat::kPNG, SkEncodedImageFormat::kJPEG, SkEncodedImageFormat::kWEBP }) { auto data = SkEncodeBitmap(bm, format, 80); auto gen = SkImageGeneratorNDK::MakeFromEncodedNDK(std::move(data)); if (!gen) { ERRORF(r, "Failed to encode!"); return; } if (!cs) cs = SkColorSpace::MakeSRGB(); REPORTER_ASSERT(r, SkColorSpace::Equals(gen->getInfo().colorSpace(), cs.get())); } } } DEF_TEST(NdkDecode_ColorSpace, r) { for (const char* path: { "images/CMYK.jpg", "images/arrow.png", "images/baby_tux.webp", "images/color_wheel.gif", "images/rle.bmp", "images/color_wheel.ico", "images/google_chrome.ico", "images/mandrill.wbmp", }) { auto gen = make_generator(path, r); if (!gen) continue; for (sk_sp cs : { sk_sp(nullptr), SkColorSpace::MakeSRGB(), SkColorSpace::MakeSRGBLinear(), SkColorSpace::MakeRGB(SkNamedTransferFn::kRec2020, SkNamedGamut::kSRGB), SkColorSpace::MakeRGB(SkNamedTransferFn::kRec2020, SkNamedGamut::kRec2020), SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDisplayP3), SkColorSpace::MakeRGB(SkNamedTransferFn::k2Dot2, SkNamedGamut::kAdobeRGB), SkColorSpace::MakeRGB(k2Dot6, kDCIP3), }) { auto info = gen->getInfo().makeColorSpace(cs); SkBitmap bm; bm.allocPixels(info); REPORTER_ASSERT(r, gen->getPixels(bm.pixmap())); } std::vector> unsupportedCs; for (auto gamut : { SkNamedGamut::kSRGB, SkNamedGamut::kAdobeRGB, SkNamedGamut::kDisplayP3, SkNamedGamut::kRec2020, SkNamedGamut::kXYZ }) { unsupportedCs.push_back(SkColorSpace::MakeRGB(SkNamedTransferFn::kPQ, gamut)); unsupportedCs.push_back(SkColorSpace::MakeRGB(SkNamedTransferFn::kHLG, gamut)); unsupportedCs.push_back(SkColorSpace::MakeRGB(k2Dot6, gamut)); } for (auto gamut : { SkNamedGamut::kSRGB, SkNamedGamut::kDisplayP3, SkNamedGamut::kRec2020, SkNamedGamut::kXYZ }) { unsupportedCs.push_back(SkColorSpace::MakeRGB(SkNamedTransferFn::k2Dot2, gamut)); } for (auto gamut : { SkNamedGamut::kAdobeRGB, SkNamedGamut::kDisplayP3, SkNamedGamut::kXYZ }) { unsupportedCs.push_back(SkColorSpace::MakeRGB(SkNamedTransferFn::kRec2020, gamut)); } for (auto gamut : { SkNamedGamut::kAdobeRGB, SkNamedGamut::kDisplayP3, SkNamedGamut::kRec2020, SkNamedGamut::kXYZ }) { unsupportedCs.push_back(SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear, gamut)); } for (auto gamut : { SkNamedGamut::kAdobeRGB, SkNamedGamut::kRec2020, SkNamedGamut::kXYZ }) { unsupportedCs.push_back(SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, gamut)); } for (auto fn : { SkNamedTransferFn::kSRGB, SkNamedTransferFn::k2Dot2, SkNamedTransferFn::kLinear, SkNamedTransferFn::kRec2020 }) { unsupportedCs.push_back(SkColorSpace::MakeRGB(fn, kDCIP3)); } for (auto unsupported : unsupportedCs) { auto info = gen->getInfo().makeColorSpace(unsupported); SkBitmap bm; bm.allocPixels(info); REPORTER_ASSERT(r, !gen->getPixels(bm.pixmap())); } } } DEF_TEST(NdkDecode_reuseNoColorSpace, r) { static const struct { const char* fPath; sk_sp fCorrectedColorSpace; bool fIsOpaque; } recs[] = { // AImageDecoder defaults to ADATASPACE_UNKNOWN for this image. {"images/wide_gamut_yellow_224_224_64.jpeg", SkColorSpace::MakeSRGB(), true}, // This image is SRGB, so convert to a different color space. {"images/example_1.png", SkColorSpace::MakeRGB(SkNamedTransferFn::k2Dot2, SkNamedGamut::kAdobeRGB), false}, }; for (auto& rec : recs) { auto gen = make_generator(rec.fPath, r); if (!gen) continue; REPORTER_ASSERT(r, gen->getInfo().colorSpace()->isSRGB()); REPORTER_ASSERT(r, gen->getInfo().isOpaque() == rec.fIsOpaque); auto noColorCorrection = gen->getInfo().makeColorSpace(nullptr); if (rec.fIsOpaque) { // Use something other than the default color type to verify that the modified color // type is used even when the color space is reset. noColorCorrection = noColorCorrection.makeColorType(kRGB_565_SkColorType); } SkBitmap orig; orig.allocPixels(noColorCorrection); REPORTER_ASSERT(r, gen->getPixels(orig.pixmap())); SkBitmap corrected; corrected.allocPixels(noColorCorrection.makeColorSpace(rec.fCorrectedColorSpace)); REPORTER_ASSERT(r, gen->getPixels(corrected.pixmap())); REPORTER_ASSERT(r, !ToolUtils::equal_pixels(orig, corrected)); SkBitmap reuse; reuse.allocPixels(noColorCorrection); REPORTER_ASSERT(r, gen->getPixels(reuse.pixmap())); REPORTER_ASSERT(r, ToolUtils::equal_pixels(orig, reuse)); } } // The NDK supports scaling up to arbitrary dimensions. Skia forces clients to do this in a // separate step, so the client is in charge of how to do the upscale. DEF_TEST(NdkDecode_noUpscale, r) { for (const char* path: { "images/CMYK.jpg", "images/arrow.png", "images/baby_tux.webp", "images/color_wheel.gif", "images/rle.bmp", "images/color_wheel.ico", "images/google_chrome.ico", "images/mandrill.wbmp", }) { auto gen = make_generator(path, r); if (!gen) continue; const auto actualDimensions = gen->getInfo().dimensions(); const int width = actualDimensions.width(); const int height = actualDimensions.height(); for (SkISize dims : { SkISize{width*2, height*2}, SkISize{width + 1, height + 1}, }) { auto info = gen->getInfo().makeDimensions(dims); SkBitmap bm; bm.allocPixels(info); REPORTER_ASSERT(r, !gen->getPixels(bm.pixmap())); } } } // libwebp supports downscaling to an arbitrary scale factor, and this is supported by the NDK. DEF_TEST(NdkDecode_webpArbitraryDownscale, r) { for (const char* path: { "images/baby_tux.webp", "images/yellow_rose.webp", "images/webp-color-profile-lossless.webp", }) { auto gen = make_generator(path, r); if (!gen) continue; const auto actualDimensions = gen->getInfo().dimensions(); const int width = actualDimensions.width(); const int height = actualDimensions.height(); for (SkISize dims : { SkISize{width/2, height/2}, SkISize{width/4, height/4}, SkISize{width/7, height/7}, SkISize{width - 1, height - 1}, SkISize{1, 1}, SkISize{5, 20} }) { auto info = gen->getInfo().makeDimensions(dims); SkBitmap bm; bm.allocPixels(info); REPORTER_ASSERT(r, gen->getPixels(bm.pixmap())); REPORTER_ASSERT(r, info.alphaType() != kUnpremul_SkAlphaType); auto unpremulInfo = info.makeAlphaType(kUnpremul_SkAlphaType); bm.allocPixels(unpremulInfo); REPORTER_ASSERT(r, gen->getPixels(bm.pixmap())); } } } // libjpeg-turbo supports downscaling to some scale factors. DEF_TEST(NdkDecode_jpegDownscale, r) { static const struct { const char* fPath; SkISize fSupportedSizes[4]; } recs[] = { {"images/CMYK.jpg", {{642,516},{321,258},{161,129},{81,65}}}, {"images/dog.jpg", {{180,180},{90,90},{45,45},{23,23}}}, {"images/grayscale.jpg", {{128,128},{64,64},{32,32},{16,16}}}, {"images/brickwork-texture.jpg", {{512,512},{256,256},{128,128},{64,64}}}, {"images/mandrill_h2v1.jpg", {{512,512},{256,256},{128,128},{64,64}}}, {"images/ducky.jpg", {{489,537},{245,269},{123,135},{62,68}}}, }; for (auto& rec : recs) { auto gen = make_generator(rec.fPath, r); if (!gen) continue; for (SkISize dims : rec.fSupportedSizes) { auto info = gen->getInfo().makeDimensions(dims); SkBitmap bm; bm.allocPixels(info); if (!gen->getPixels(bm.pixmap())) { ERRORF(r, "failed to decode %s to {%i,%i}\n", rec.fPath, dims.width(), dims.height()); } REPORTER_ASSERT(r, info.alphaType() != kUnpremul_SkAlphaType); auto unpremulInfo = info.makeAlphaType(kUnpremul_SkAlphaType); bm.allocPixels(unpremulInfo); REPORTER_ASSERT(r, gen->getPixels(bm.pixmap())); } } } DEF_TEST(NdkDecode_reuseJpeg, r) { auto gen = make_generator("images/CMYK.jpg", r); if (!gen) return; SkImageInfo info = gen->getInfo(); SkBitmap orig; orig.allocPixels(info); REPORTER_ASSERT(r, gen->getPixels(orig.pixmap())); info = info.makeWH(321, 258); SkBitmap downscaled; downscaled.allocPixels(info); REPORTER_ASSERT(r, gen->getPixels(downscaled.pixmap())); SkBitmap reuse; reuse.allocPixels(gen->getInfo()); REPORTER_ASSERT(r, gen->getPixels(reuse.pixmap())); REPORTER_ASSERT(r, ToolUtils::equal_pixels(orig, reuse)); } // The NDK supports scaling down to arbitrary dimensions. Skia forces clients to do this in a // separate step, so the client is in charge of how to do the downscale. DEF_TEST(NdkDecode_noDownscale, r) { for (const char* path: { "images/arrow.png", "images/color_wheel.gif", "images/rle.bmp", "images/color_wheel.ico", "images/google_chrome.ico", "images/mandrill.wbmp", }) { auto gen = make_generator(path, r); if (!gen) continue; const auto actualDimensions = gen->getInfo().dimensions(); const int width = actualDimensions.width(); const int height = actualDimensions.height(); for (SkISize dims : { SkISize{width/2, height/2}, SkISize{width/3, height/3}, SkISize{width/4, height/4}, SkISize{width/8, height/8}, SkISize{width - 1, height - 1}, }) { auto info = gen->getInfo().makeDimensions(dims); SkBitmap bm; bm.allocPixels(info); REPORTER_ASSERT(r, !gen->getPixels(bm.pixmap())); } } } DEF_TEST(NdkDecode_Gray8, r) { static const struct { const char* fPath; bool fGrayscale; } recs[] = { {"images/CMYK.jpg", false}, {"images/arrow.png", false}, {"images/baby_tux.webp", false}, {"images/color_wheel.gif", false}, {"images/rle.bmp", false}, {"images/color_wheel.ico", false}, {"images/google_chrome.ico", false}, {"images/mandrill.wbmp", true}, {"images/grayscale.jpg", true}, {"images/grayscale.png", true}, }; for (auto& rec : recs) { auto gen = make_generator(rec.fPath, r); if (!gen) continue; SkImageInfo info = gen->getInfo(); if (rec.fGrayscale) { REPORTER_ASSERT(r, info.colorType() == kGray_8_SkColorType); REPORTER_ASSERT(r, info.alphaType() == kOpaque_SkAlphaType); } else { info = info.makeColorType(kGray_8_SkColorType); } SkBitmap bm; bm.allocPixels(info); bool success = gen->getPixels(bm.pixmap()); if (success != rec.fGrayscale) { ERRORF(r, "Expected decoding %s to Gray8 to %s. Actual: %s\n", rec.fPath, (rec.fGrayscale ? "succeed" : "fail"), (success ? "succeed" : "fail")); } } } DEF_TEST(NdkDecode_Opaque_and_565, r) { for (const char* path: { "images/CMYK.jpg", "images/dog.jpg", "images/ducky.jpg", "images/arrow.png", "images/example_1.png", "images/explosion_sprites.png", "images/lut_identity.png", "images/grayscale.png", "images/baby_tux.webp", "images/yellow_rose.webp", "images/webp-color-profile-lossless.webp", "images/colorTables.gif", "images/color_wheel.gif", "images/flightAnim.gif", "images/randPixels.gif", "images/rle.bmp", "images/color_wheel.ico", "images/google_chrome.ico", "images/mandrill.wbmp", }) { auto gen = make_generator(path, r); if (!gen) continue; auto info = gen->getInfo().makeAlphaType(kOpaque_SkAlphaType); SkBitmap bm; bm.allocPixels(info); bool success = gen->getPixels(bm.pixmap()); REPORTER_ASSERT(r, success == gen->getInfo().isOpaque()); info = info.makeColorType(kRGB_565_SkColorType); bm.allocPixels(info); success = gen->getPixels(bm.pixmap()); REPORTER_ASSERT(r, success == gen->getInfo().isOpaque()); } } DEF_TEST(NdkDecode_AlwaysSupportedColorTypes, r) { for (const char* path: { "images/CMYK.jpg", "images/dog.jpg", "images/ducky.jpg", "images/arrow.png", "images/example_1.png", "images/explosion_sprites.png", "images/lut_identity.png", "images/grayscale.png", "images/baby_tux.webp", "images/yellow_rose.webp", "images/webp-color-profile-lossless.webp", "images/colorTables.gif", "images/color_wheel.gif", "images/flightAnim.gif", "images/randPixels.gif", "images/rle.bmp", "images/color_wheel.ico", "images/google_chrome.ico", "images/mandrill.wbmp", }) { auto gen = make_generator(path, r); if (!gen) continue; auto info = gen->getInfo().makeColorType(kRGBA_F16_SkColorType); SkBitmap bm; bm.allocPixels(info); REPORTER_ASSERT(r, gen->getPixels(bm.pixmap())); // This also tests that we can reuse the same generator for a different // color type. info = info.makeColorType(kRGBA_8888_SkColorType); bm.allocPixels(info); REPORTER_ASSERT(r, gen->getPixels(bm.pixmap())); } } DEF_TEST(NdkDecode_UnsupportedColorTypes, r) { for (const char* path: { "images/CMYK.jpg", "images/dog.jpg", "images/ducky.jpg", "images/arrow.png", "images/example_1.png", "images/explosion_sprites.png", "images/lut_identity.png", "images/grayscale.png", "images/baby_tux.webp", "images/yellow_rose.webp", "images/webp-color-profile-lossless.webp", "images/colorTables.gif", "images/color_wheel.gif", "images/flightAnim.gif", "images/randPixels.gif", "images/rle.bmp", "images/color_wheel.ico", "images/google_chrome.ico", "images/mandrill.wbmp", }) { auto gen = make_generator(path, r); if (!gen) continue; for (SkColorType ct : { kUnknown_SkColorType, kAlpha_8_SkColorType, kARGB_4444_SkColorType, kRGB_888x_SkColorType, kBGRA_8888_SkColorType, kRGBA_1010102_SkColorType, kBGRA_1010102_SkColorType, kRGB_101010x_SkColorType, kBGR_101010x_SkColorType, kRGBA_F16Norm_SkColorType, kRGBA_F32_SkColorType, kR8G8_unorm_SkColorType, kA16_float_SkColorType, kR16G16_float_SkColorType, kA16_unorm_SkColorType, kR16G16_unorm_SkColorType, kR16G16B16A16_unorm_SkColorType, }) { auto info = gen->getInfo().makeColorType(ct); SkBitmap bm; bm.allocPixels(info); if (gen->getPixels(bm.pixmap())) { ERRORF(r, "Expected decoding %s to %i to fail!", path, ct); } } } } #endif // SK_ENABLE_NDK_IMAGES