diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/core/DownsampleMode.kt b/imagepipeline/src/main/java/com/facebook/imagepipeline/core/DownsampleMode.kt new file mode 100644 index 0000000000..8f023ea6b2 --- /dev/null +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/core/DownsampleMode.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.imagepipeline.core + +enum class DownsampleMode { + ALWAYS, + AUTO, + NEVER +} diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineConfig.kt b/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineConfig.kt index e9d2a264f3..51b6d7a9d9 100644 --- a/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineConfig.kt +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineConfig.kt @@ -574,9 +574,3 @@ class ImagePipelineConfig private constructor(builder: Builder) : ImagePipelineC } } } - -enum class DownsampleMode { - ALWAYS, - AUTO, - NEVER -} diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/producers/DecodeProducer.kt b/imagepipeline/src/main/java/com/facebook/imagepipeline/producers/DecodeProducer.kt index 02f32c7734..fdb7f55981 100644 --- a/imagepipeline/src/main/java/com/facebook/imagepipeline/producers/DecodeProducer.kt +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/producers/DecodeProducer.kt @@ -396,25 +396,27 @@ class DecodeProducer( protected abstract val qualityInfo: QualityInfo init { - val job = JobRunnable { encodedImage, status -> if (encodedImage != null) { val request = producerContext.imageRequest producerContext.putExtra(HasExtraData.KEY_IMAGE_FORMAT, encodedImage.imageFormat.name) encodedImage.source = request.sourceUri?.toString() + val requestDownsampleMode = request.downsampleOverride ?: downsampleMode val isResizingDone = statusHasFlag(status, IS_RESIZING_DONE) - if (downsampleMode == DownsampleMode.ALWAYS || - (downsampleMode == DownsampleMode.AUTO && !isResizingDone)) { - if (downsampleEnabledForNetwork || !UriUtil.isNetworkUri(request.sourceUri)) { - encodedImage.sampleSize = - DownsampleUtil.determineSampleSize( - request.rotationOptions, - request.resizeOptions, - encodedImage, - maxBitmapDimension) - } + val shouldAdjustSampleSize = + (requestDownsampleMode == DownsampleMode.ALWAYS || + (requestDownsampleMode == DownsampleMode.AUTO && !isResizingDone)) && + (downsampleEnabledForNetwork || !UriUtil.isNetworkUri(request.sourceUri)) + if (shouldAdjustSampleSize) { + encodedImage.sampleSize = + DownsampleUtil.determineSampleSize( + request.rotationOptions, + request.resizeOptions, + encodedImage, + maxBitmapDimension) } + if (producerContext.imagePipelineConfig.experiments.downsampleIfLargeBitmap) { maybeIncreaseSampleSize(encodedImage) } diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/request/ImageRequest.java b/imagepipeline/src/main/java/com/facebook/imagepipeline/request/ImageRequest.java index 044c6dee65..0c6b5c1ea4 100644 --- a/imagepipeline/src/main/java/com/facebook/imagepipeline/request/ImageRequest.java +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/request/ImageRequest.java @@ -32,6 +32,7 @@ import com.facebook.imagepipeline.common.ResizeOptions; import com.facebook.imagepipeline.common.RotationOptions; import com.facebook.imagepipeline.common.SourceUriType; +import com.facebook.imagepipeline.core.DownsampleMode; import com.facebook.imagepipeline.listener.RequestListener; import com.facebook.imageutils.BitmapUtil; import com.facebook.infer.annotation.Nullsafe; @@ -120,6 +121,9 @@ public class ImageRequest { */ private final @Nullable Boolean mResizingAllowedOverride; + /** Custom downsample override for this request. null -> use default pipeline's setting. */ + private final @Nullable DownsampleMode mDownsampleOverride; + private final @Nullable String mDiskCacheId; private final int mDelayMs; @@ -175,6 +179,8 @@ protected ImageRequest(ImageRequestBuilder builder) { mResizingAllowedOverride = builder.getResizingAllowedOverride(); + mDownsampleOverride = builder.getDownsampleOverride(); + mDelayMs = builder.getDelayMs(); mDiskCacheId = builder.getDiskCacheId(); @@ -270,6 +276,10 @@ public boolean isMemoryCacheEnabled() { return mResizingAllowedOverride; } + public @Nullable DownsampleMode getDownsampleOverride() { + return mDownsampleOverride; + } + public int getDelayMs() { return mDelayMs; } @@ -322,6 +332,7 @@ public boolean equals(@Nullable Object o) { || !Objects.equal(mCachesDisabled, request.mCachesDisabled) || !Objects.equal(mDecodePrefetches, request.mDecodePrefetches) || !Objects.equal(mResizingAllowedOverride, request.mResizingAllowedOverride) + || !Objects.equal(mDownsampleOverride, request.mDownsampleOverride) || !Objects.equal(mRotationOptions, request.mRotationOptions) || mLoadThumbnailOnly != request.mLoadThumbnailOnly) { return false; @@ -359,6 +370,7 @@ public int hashCode() { result = HashCode.extend(result, mRotationOptions); result = HashCode.extend(result, postprocessorCacheKey); result = HashCode.extend(result, mResizingAllowedOverride); + result = HashCode.extend(result, mDownsampleOverride); result = HashCode.extend(result, mDelayMs); result = HashCode.extend(result, mLoadThumbnailOnly); // ^ I *think* this is safe despite autoboxing...? @@ -393,6 +405,7 @@ public void recordHashCode(HashMap hashCodeLog) { hashCodeLog.put("ImageRequest.postprocessorCacheKey", getHashCodeHelper(postprocessorCacheKey)); hashCodeLog.put( "ImageRequest.mResizingAllowedOverride", getHashCodeHelper(mResizingAllowedOverride)); + hashCodeLog.put("ImageRequest.mDownsampleOverride", getHashCodeHelper(mDownsampleOverride)); hashCodeLog.put("ImageRequest.mDelayMs", getHashCodeHelper(mDelayMs)); hashCodeLog.put("ImageRequest.mLoadThumbnailOnly", getHashCodeHelper(mLoadThumbnailOnly)); } @@ -417,6 +430,7 @@ public String toString() { .add("rotationOptions", mRotationOptions) .add("bytesRange", mBytesRange) .add("resizingAllowedOverride", mResizingAllowedOverride) + .add("downsampleOverride", mDownsampleOverride) .add("progressiveRenderingEnabled", mProgressiveRenderingEnabled) .add("localThumbnailPreviewsEnabled", mLocalThumbnailPreviewsEnabled) .add("loadThumbnailOnly", mLoadThumbnailOnly) diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/request/ImageRequestBuilder.java b/imagepipeline/src/main/java/com/facebook/imagepipeline/request/ImageRequestBuilder.java index 42e177fb5b..1a8ed81e3f 100644 --- a/imagepipeline/src/main/java/com/facebook/imagepipeline/request/ImageRequestBuilder.java +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/request/ImageRequestBuilder.java @@ -19,6 +19,7 @@ import com.facebook.imagepipeline.common.Priority; import com.facebook.imagepipeline.common.ResizeOptions; import com.facebook.imagepipeline.common.RotationOptions; +import com.facebook.imagepipeline.core.DownsampleMode; import com.facebook.imagepipeline.core.ImagePipelineConfig; import com.facebook.imagepipeline.core.ImagePipelineExperiments; import com.facebook.imagepipeline.listener.RequestListener; @@ -47,6 +48,7 @@ public class ImageRequestBuilder { private @Nullable RequestListener mRequestListener; private @Nullable BytesRange mBytesRange = null; private @Nullable Boolean mResizingAllowedOverride = null; + private @Nullable DownsampleMode mDownsampleOverride = null; private int mDelayMs; private @Nullable String mDiskCacheId = null; @@ -104,7 +106,8 @@ public static ImageRequestBuilder fromRequest(ImageRequest imageRequest) { .setRotationOptions(imageRequest.getRotationOptions()) .setShouldDecodePrefetches(imageRequest.shouldDecodePrefetches()) .setDelayMs(imageRequest.getDelayMs()) - .setDiskCacheId(imageRequest.getDiskCacheId()); + .setDiskCacheId(imageRequest.getDiskCacheId()) + .setDownsampleOverride(imageRequest.getDownsampleOverride()); } public static void addCustomUriNetworkScheme(String scheme) { @@ -453,6 +456,15 @@ public ImageRequestBuilder setResizingAllowedOverride(@Nullable Boolean resizing return mResizingAllowedOverride; } + public ImageRequestBuilder setDownsampleOverride(@Nullable DownsampleMode downsampleOverride) { + this.mDownsampleOverride = downsampleOverride; + return this; + } + + public @Nullable DownsampleMode getDownsampleOverride() { + return mDownsampleOverride; + } + public int getDelayMs() { return mDelayMs; } diff --git a/imagepipeline/src/test/java/com/facebook/imagepipeline/producers/DecodeProducerTest.java b/imagepipeline/src/test/java/com/facebook/imagepipeline/producers/DecodeProducerTest.java index 4922a664cb..a0b3b49dd3 100644 --- a/imagepipeline/src/test/java/com/facebook/imagepipeline/producers/DecodeProducerTest.java +++ b/imagepipeline/src/test/java/com/facebook/imagepipeline/producers/DecodeProducerTest.java @@ -413,6 +413,22 @@ public void testDecode_WhenSmartResizingEnabledAndLocalUri_ThenPerformDownsampli assertNotEquals(mEncodedImage.getSampleSize(), EncodedImage.DEFAULT_SAMPLE_SIZE); } + @Test + public void testDecode_WhenDownsampleOverrideProvidedAndLocalUri_ThenPerformNoDownsampling() + throws Exception { + int resizedWidth = 10; + int resizedHeight = 10; + setupLocalUri(ResizeOptions.forDimensions(resizedWidth, resizedHeight), DownsampleMode.NEVER); + + produceResults(); + JobScheduler.JobRunnable jobRunnable = getJobRunnable(); + + jobRunnable.run(mEncodedImage, Consumer.IS_LAST); + + // The sample size was not modified, which means Downsampling has not been performed + assertEquals(mEncodedImage.getSampleSize(), EncodedImage.DEFAULT_SAMPLE_SIZE); + } + @Test public void testDecode_WhenSmartResizingEnabledAndNetworkUri_ThenPerformNoDownsampling() throws Exception { @@ -429,6 +445,23 @@ public void testDecode_WhenSmartResizingEnabledAndNetworkUri_ThenPerformNoDownsa assertEquals(mEncodedImage.getSampleSize(), EncodedImage.DEFAULT_SAMPLE_SIZE); } + @Test + public void testDecode_WhenDownsampleOverrideProvidedAndNetworkUri_ThenPerformNoDownsampling() + throws Exception { + int resizedWidth = 10; + int resizedHeight = 10; + setupNetworkUri( + ResizeOptions.forDimensions(resizedWidth, resizedHeight), DownsampleMode.ALWAYS); + + produceResults(); + JobScheduler.JobRunnable jobRunnable = getJobRunnable(); + + jobRunnable.run(mEncodedImage, Consumer.IS_LAST); + + // The sample size was not modified, which means Downsampling has not been performed + assertEquals(mEncodedImage.getSampleSize(), EncodedImage.DEFAULT_SAMPLE_SIZE); + } + private void setupImageRequest(String requestId, ImageRequest imageRequest) { mImageRequest = imageRequest; mRequestId = requestId; @@ -446,30 +479,42 @@ private void setupImageRequest(String requestId, ImageRequest imageRequest) { } private void setupNetworkUri() { - setupNetworkUri(null); + setupNetworkUri(null, null); } private void setupNetworkUri(@Nullable ResizeOptions resizeOptions) { + setupNetworkUri(resizeOptions, null); + } + + private void setupNetworkUri( + @Nullable ResizeOptions resizeOptions, @Nullable DownsampleMode downsampleOverride) { setupImageRequest( "networkRequest1", ImageRequestBuilder.newBuilderWithSource(Uri.parse("http://www.fb.com/image")) .setProgressiveRenderingEnabled(true) .setImageDecodeOptions(IMAGE_DECODE_OPTIONS) .setResizeOptions(resizeOptions) + .setDownsampleOverride(downsampleOverride) .build()); } private void setupLocalUri() { - setupLocalUri(null); + setupLocalUri(null, null); } private void setupLocalUri(@Nullable ResizeOptions resizeOptions) { + setupLocalUri(resizeOptions, null); + } + + private void setupLocalUri( + @Nullable ResizeOptions resizeOptions, @Nullable DownsampleMode downsampleOverride) { setupImageRequest( "localRequest1", ImageRequestBuilder.newBuilderWithSource(Uri.parse("file://path/image")) .setProgressiveRenderingEnabled(true) // this should be ignored .setImageDecodeOptions(IMAGE_DECODE_OPTIONS) .setResizeOptions(resizeOptions) + .setDownsampleOverride(downsampleOverride) .build()); } diff --git a/vito/core-java-impl/src/main/java/com/facebook/fresco/vito/core/impl/ImagePipelineUtilsImpl.kt b/vito/core-java-impl/src/main/java/com/facebook/fresco/vito/core/impl/ImagePipelineUtilsImpl.kt index 7faf4477cc..bb6fbe895b 100644 --- a/vito/core-java-impl/src/main/java/com/facebook/fresco/vito/core/impl/ImagePipelineUtilsImpl.kt +++ b/vito/core-java-impl/src/main/java/com/facebook/fresco/vito/core/impl/ImagePipelineUtilsImpl.kt @@ -61,6 +61,7 @@ class ImagePipelineUtilsImpl(private val imageDecodeOptionsProvider: ImageDecode ): ImageRequestBuilder? = imageRequestBuilder?.apply { imageOptions.resizeOptions?.let { resizeOptions = it } + imageOptions.downsampleOverride?.let { downsampleOverride = it } imageOptions.rotationOptions?.let { rotationOptions = it } imageDecodeOptionsProvider.create(imageRequestBuilder, imageOptions)?.let { imageDecodeOptions = it diff --git a/vito/core-java-impl/src/test/java/com/facebook/fresco/vito/core/impl/ImagePipelineUtilsImplTest.kt b/vito/core-java-impl/src/test/java/com/facebook/fresco/vito/core/impl/ImagePipelineUtilsImplTest.kt index 52430d6644..f1cd997d89 100644 --- a/vito/core-java-impl/src/test/java/com/facebook/fresco/vito/core/impl/ImagePipelineUtilsImplTest.kt +++ b/vito/core-java-impl/src/test/java/com/facebook/fresco/vito/core/impl/ImagePipelineUtilsImplTest.kt @@ -15,6 +15,7 @@ import com.facebook.fresco.vito.options.RoundingOptions import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.common.ResizeOptions import com.facebook.imagepipeline.common.RotationOptions +import com.facebook.imagepipeline.core.DownsampleMode import com.facebook.imagepipeline.testing.TestNativeLoader import org.assertj.core.api.Java6Assertions import org.assertj.core.api.Java6Assertions.fail @@ -142,6 +143,22 @@ class ImagePipelineUtilsImplTest { Java6Assertions.assertThat(imageRequest.resizeOptions).isEqualTo(resizeOptions) } + @Test + fun testBuildImageRequest_whenResizingOverrideDisabled_thenSetOverrideOption() { + val resizeOptions = ResizeOptions.forDimensions(123, 234) + val imageOptions = + ImageOptions.create().resize(resizeOptions).downsampleOverride(DownsampleMode.NEVER).build() + val imageRequest = imagePipelineUtils.buildImageRequest(URI, imageOptions) + if (imageRequest == null) { + fail("not null value expected") + return + } + + Java6Assertions.assertThat(imageRequest.sourceUri).isEqualTo(URI) + Java6Assertions.assertThat(imageRequest.resizeOptions).isEqualTo(resizeOptions) + Java6Assertions.assertThat(imageRequest.downsampleOverride).isEqualTo(DownsampleMode.NEVER) + } + @Test fun testBuildImageRequest_whenRotatingEnabled_thenSetRotateOptions() { val rotationOptions = RotationOptions.forceRotation(RotationOptions.ROTATE_270) diff --git a/vito/options/src/main/java/com/facebook/fresco/vito/options/DecodedImageOptions.kt b/vito/options/src/main/java/com/facebook/fresco/vito/options/DecodedImageOptions.kt index a234cb91ba..0681791653 100644 --- a/vito/options/src/main/java/com/facebook/fresco/vito/options/DecodedImageOptions.kt +++ b/vito/options/src/main/java/com/facebook/fresco/vito/options/DecodedImageOptions.kt @@ -14,10 +14,12 @@ import com.facebook.drawee.drawable.ScalingUtils import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.common.ResizeOptions import com.facebook.imagepipeline.common.RotationOptions +import com.facebook.imagepipeline.core.DownsampleMode import com.facebook.imagepipeline.request.Postprocessor open class DecodedImageOptions(builder: Builder<*>) : EncodedImageOptions(builder) { val resizeOptions: ResizeOptions? = builder.resizeOptions + val downsampleOverride: DownsampleMode? = builder.downsampleOverride val rotationOptions: RotationOptions? = builder.rotationOptions val postprocessor: Postprocessor? = builder.postprocessor val imageDecodeOptions: ImageDecodeOptions? = builder.imageDecodeOptions @@ -42,6 +44,7 @@ open class DecodedImageOptions(builder: Builder<*>) : EncodedImageOptions(builde protected fun equalDecodedOptions(other: DecodedImageOptions): Boolean { return if (!Objects.equal(resizeOptions, other.resizeOptions) || + !Objects.equal(downsampleOverride, other.downsampleOverride) || !Objects.equal(rotationOptions, other.rotationOptions) || !Objects.equal(postprocessor, other.postprocessor) || !Objects.equal(imageDecodeOptions, other.imageDecodeOptions) || @@ -60,6 +63,7 @@ open class DecodedImageOptions(builder: Builder<*>) : EncodedImageOptions(builde override fun hashCode(): Int { var result = super.hashCode() result = 31 * result + (resizeOptions?.hashCode() ?: 0) + result = 31 * result + (downsampleOverride?.hashCode() ?: 0) result = 31 * result + (rotationOptions?.hashCode() ?: 0) result = 31 * result + (postprocessor?.hashCode() ?: 0) result = 31 * result + (imageDecodeOptions?.hashCode() ?: 0) @@ -79,6 +83,7 @@ open class DecodedImageOptions(builder: Builder<*>) : EncodedImageOptions(builde override fun toStringHelper(): Objects.ToStringHelper = super.toStringHelper() .add("resizeOptions", resizeOptions) + .add("downsampleOverride", downsampleOverride) .add("rotationOptions", resizeOptions) .add("postprocessor", postprocessor) .add("imageDecodeOptions", imageDecodeOptions) @@ -93,6 +98,7 @@ open class DecodedImageOptions(builder: Builder<*>) : EncodedImageOptions(builde open class Builder> : EncodedImageOptions.Builder { internal var resizeOptions: ResizeOptions? = null + internal var downsampleOverride: DownsampleMode? = null internal var rotationOptions: RotationOptions? = null internal var postprocessor: Postprocessor? = null internal var imageDecodeOptions: ImageDecodeOptions? = null @@ -109,6 +115,7 @@ open class DecodedImageOptions(builder: Builder<*>) : EncodedImageOptions(builde constructor(decodedImageOptions: DecodedImageOptions) : super(decodedImageOptions) { resizeOptions = decodedImageOptions.resizeOptions + downsampleOverride = decodedImageOptions.downsampleOverride rotationOptions = decodedImageOptions.rotationOptions postprocessor = decodedImageOptions.postprocessor imageDecodeOptions = decodedImageOptions.imageDecodeOptions @@ -126,6 +133,16 @@ open class DecodedImageOptions(builder: Builder<*>) : EncodedImageOptions(builde fun resize(resizeOptions: ResizeOptions?): T = modify { this.resizeOptions = resizeOptions } + /** + * Custom downsample override for this request. null -> use default pipeline's setting. + * + * @param downsampleOverride + * @return the builder + */ + fun downsampleOverride(downsampleOverride: DownsampleMode?): T = modify { + this.downsampleOverride = downsampleOverride + } + fun rotate(rotationOptions: RotationOptions?): T = modify { this.rotationOptions = rotationOptions }