From f543954b4be8228a8bfe95eb897ccd81e8d89da6 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Wed, 15 Jul 2020 03:14:54 +0000 Subject: [PATCH] Bug 1650692 - ImageDecoder improvements, r=agi,geckoview-reviewers,snorp Differential Revision: https://phabricator.services.mozilla.com/D82308 --- .../web_extensions/actions/button/red.png | Bin 0 -> 1731 bytes .../geckoview/test/ImageDecoderTest.kt | 123 ++++++++++++++++++ .../org/mozilla/geckoview/ImageDecoder.java | 20 +-- settings.gradle | 2 +- widget/android/ImageDecoderSupport.cpp | 25 ++-- widget/android/ImageDecoderSupport.h | 2 +- 6 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/red.png create mode 100644 mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ImageDecoderTest.kt diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/red.png b/mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/red.png new file mode 100644 index 0000000000000000000000000000000000000000..14d65ca99cc1126c7504148b681bab0673fdb4fb GIT binary patch literal 1731 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj{$|z_KYbB%&n3*T*V3KUXg?B|j-u zuOhbqD9^xPV_#8_n4FzjqL7rDo|$K>^nUk#C56lsTcvPQUjyF)=hTc$kE){7;3~h6 zMhJ1(0FE1&_nsU?XD6}dTi#a0!zN?>!XfNYSkzLEl1NlCV?QiN}Sf^&XR zs)DJWsh)w79hZVlQA(Oskc%5sGmvMilu=SrV5P5LUS6(OZmgGIl&)`RX=$l%V5Dzk zqzhD`TU?n}l31aeSF8*&0%C?sYH@N=W@A>i5ZU7Vw370~qEyFpUou(C`N`VI#Z}j-Uw6#O9=a$NO8K=JE)to{(thXDQG9HJ%h+nWaL6@UTsbgyE z)*O#A|kgKTvSm8v4# z=B&B-e}C;&^WWH}IN!Kuz4GA`^37j4^(HUF%-Gx5_SETo*Y7zt4fW3ZUP*oXwlMsh z`T9Q$+3V9^OsTX>1J)Rv1s;*b3=G`DAk4@xYmNj^P{Y&3F~p() + val svg100 = GeckoResult() + + ImageDecoder.instance().decode("web_extensions/actions/button/icon.svg").accept { actual -> + assertEquals(500, actual!!.width) + assertEquals(500, actual.height) + svgNatural.complete(null) + } + + ImageDecoder.instance().decode("web_extensions/actions/button/icon.svg", 100).accept { actual -> + assertEquals(100, actual!!.width) + assertEquals(100, actual.height) + compareBitmap("web_extensions/actions/button/expected.png", actual) + svg100.complete(null) + } + + UiThreadUtils.waitForResult(svgNatural) + UiThreadUtils.waitForResult(svg100) + } + + @Test + fun decodeIconPng() { + val pngNatural = GeckoResult() + val png100 = GeckoResult() + val png38 = GeckoResult() + val png19 = GeckoResult() + + ImageDecoder.instance().decode("web_extensions/actions/button/geo-19.png").accept { actual -> + compareBitmap("web_extensions/actions/button/geo-19.png", actual!!) + pngNatural.complete(null) + } + + // Raster images shouldn't be upscaled + ImageDecoder.instance().decode("web_extensions/actions/button/geo-38.png", 100).accept { actual -> + compareBitmap("web_extensions/actions/button/geo-38.png", actual!!) + png100.complete(null) + } + + ImageDecoder.instance().decode("web_extensions/actions/button/geo-38.png", 38).accept { actual -> + compareBitmap("web_extensions/actions/button/geo-38.png", actual!!) + png38.complete(null) + } + + ImageDecoder.instance().decode("web_extensions/actions/button/geo-19.png", 19).accept { actual -> + compareBitmap("web_extensions/actions/button/geo-19.png", actual!!) + png19.complete(null) + } + + UiThreadUtils.waitForResult(pngNatural) + UiThreadUtils.waitForResult(png100) + UiThreadUtils.waitForResult(png38) + UiThreadUtils.waitForResult(png19) + } + + @Test + fun decodeIconPngDownscale() { + val pngNatural = GeckoResult() + val png16 = GeckoResult() + + ImageDecoder.instance().decode("web_extensions/actions/button/red.png").accept { actual -> + assertEquals(32, actual!!.width) + assertEquals(32, actual.height) + + for (x in 0 until actual.height) { + for (y in 0 until actual.width) { + assertEquals(Color.RED, actual.getPixel(x, y)) + } + } + pngNatural.complete(null) + } + + ImageDecoder.instance().decode("web_extensions/actions/button/red.png", 16).accept { actual -> + assertEquals(16, actual!!.width) + assertEquals(16, actual.height) + + for (x in 0 until actual.height) { + for (y in 0 until actual.width) { + assertEquals(Color.RED, actual.getPixel(x, y)) + } + } + png16.complete(null) + } + + UiThreadUtils.waitForResult(pngNatural) + UiThreadUtils.waitForResult(png16) + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ImageDecoder.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ImageDecoder.java index bd9d779aee69..aa085710b8d0 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ImageDecoder.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ImageDecoder.java @@ -16,6 +16,11 @@ import org.mozilla.gecko.annotation.WrapForJNI; private ImageDecoder() {} + /** + * Gets the singleton instance of this class. + * + * @return the singleton {@link ImageDecoder} instance + */ public static ImageDecoder instance() { if (instance == null) { instance = new ImageDecoder(); @@ -25,7 +30,7 @@ import org.mozilla.gecko.annotation.WrapForJNI; } @WrapForJNI(dispatchTo = "gecko", stubName = "Decode") - private static native void nativeDecode(final String uri, final int desiredLength, + private static native void nativeDecode(final String uri, final int maxSize, GeckoResult result); /** @@ -55,14 +60,13 @@ import org.mozilla.gecko.annotation.WrapForJNI; * * e.g. if the image file is locate at /assets/test.png inside the apk, set the uri * to resource://android/assets/test.png. - * @param desiredLength Longest size for the image in device pixel units. The resulting image - * might be slightly different if the image cannot be resized efficiently. - * If desiredLength is 0 then the image will be decoded to its natural - * size. + * @param maxSize Longest size for the image in device pixel units. The resulting image + * might be slightly different if the image cannot be resized efficiently. + * If maxSize is 0 then the image will be decoded to its natural size. * @return A {@link GeckoResult} to the decoded image. */ @NonNull - public GeckoResult decode(final @NonNull String uri, final int desiredLength) { + public GeckoResult decode(final @NonNull String uri, final int maxSize) { if (uri == null) { throw new IllegalArgumentException("Uri cannot be null"); } @@ -70,10 +74,10 @@ import org.mozilla.gecko.annotation.WrapForJNI; final GeckoResult result = new GeckoResult<>(); if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) { - nativeDecode(uri, desiredLength, result); + nativeDecode(uri, maxSize, result); } else { GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, this, - "nativeDecode", String.class, uri, int.class, desiredLength, + "nativeDecode", String.class, uri, int.class, maxSize, GeckoResult.class, result); } diff --git a/settings.gradle b/settings.gradle index e4d7d13c2fa9..a6d1c39f3d5f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -68,4 +68,4 @@ gradle.ext.mozconfig = json def orig = slurper.parse(new File(json.topobjdir, '.mozconfig.json')) gradle.ext.mozconfig.orig_mozconfig = orig.mozconfig project(':messaging_example').projectDir = new File('mobile/android/examples/messaging_example/app') -project(':port_messaging_example').projectDir = new File('mobile/android/examples/port_messaging_example/app') \ No newline at end of file +project(':port_messaging_example').projectDir = new File('mobile/android/examples/port_messaging_example/app') diff --git a/widget/android/ImageDecoderSupport.cpp b/widget/android/ImageDecoderSupport.cpp index 9e850a34dace..5b78c8efd52b 100644 --- a/widget/android/ImageDecoderSupport.cpp +++ b/widget/android/ImageDecoderSupport.cpp @@ -47,8 +47,8 @@ class ImageCallbackHelper : public imgIContainerCallback, gDecodeRequests.remove(this); } - ImageCallbackHelper(java::GeckoResult::Param aResult, int32_t aDesiredLength) - : mResult(aResult), mDesiredLength(aDesiredLength), mImage(nullptr) { + ImageCallbackHelper(java::GeckoResult::Param aResult, int32_t aMaxSize) + : mResult(aResult), mMaxSize(aMaxSize), mImage(nullptr) { MOZ_ASSERT(mResult); } @@ -63,20 +63,21 @@ class ImageCallbackHelper : public imgIContainerCallback, } mImage = aImage; - return mImage->StartDecoding( - imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY, - imgIContainer::FRAME_FIRST); + return mImage->StartDecoding(imgIContainer::FLAG_SYNC_DECODE | + imgIContainer::FLAG_ASYNC_NOTIFY | + imgIContainer::FLAG_HIGH_QUALITY_SCALING, + imgIContainer::FRAME_FIRST); } // This method assumes that the image is ready to be processed nsresult SendBitmap() { RefPtr surface; - if (mDesiredLength > 0) { + if (mMaxSize > 0) { surface = mImage->GetFrameAtSize( - gfx::IntSize(mDesiredLength, mDesiredLength), - imgIContainer::FRAME_FIRST, - imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + gfx::IntSize(mMaxSize, mMaxSize), imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY | + imgIContainer::FLAG_HIGH_QUALITY_SCALING); } else { surface = mImage->GetFrame( imgIContainer::FRAME_FIRST, @@ -125,7 +126,7 @@ class ImageCallbackHelper : public imgIContainerCallback, private: const java::GeckoResult::GlobalRef mResult; - int32_t mDesiredLength; + int32_t mMaxSize; nsCOMPtr mImage; virtual ~ImageCallbackHelper() {} }; @@ -136,11 +137,11 @@ NS_IMPL_ISUPPORTS(ImageCallbackHelper, imgIContainerCallback, } // namespace /* static */ void ImageDecoderSupport::Decode(jni::String::Param aUri, - int32_t aDesiredLength, + int32_t aMaxSize, jni::Object::Param aResult) { auto result = java::GeckoResult::LocalRef(aResult); RefPtr helper = - new ImageCallbackHelper(result, aDesiredLength); + new ImageCallbackHelper(result, aMaxSize); nsresult rv = DecodeInternal(aUri->ToString(), helper, helper); if (NS_FAILED(rv)) { diff --git a/widget/android/ImageDecoderSupport.h b/widget/android/ImageDecoderSupport.h index d38b3e7e1b45..64bd549aef12 100644 --- a/widget/android/ImageDecoderSupport.h +++ b/widget/android/ImageDecoderSupport.h @@ -15,7 +15,7 @@ namespace widget { class ImageDecoderSupport final : public java::ImageDecoder::Natives { public: - static void Decode(jni::String::Param aUri, int32_t aDesiredLength, + static void Decode(jni::String::Param aUri, int32_t aMaxSize, jni::Object::Param aResult); private: