Skip to content

Commit

Permalink
Hybrid image aliasing (facebook#44803)
Browse files Browse the repository at this point in the history
Summary:

This change introduces a new prop to the Android `Image` component: `resizeMultiplier`. This prop can be used when the `resizeMethod` is set to `resize`, and it directly modifies the resultant bitmap generated in memory from Fresco to be larger (or smaller) depending on the multiplier. A default of 1.0 means the bitmap size is designed to fit the destination dimensions. A multiplier greater than 1.0 will set the `ResizeOptions` provided to Fresco to be larger that the destination dimensions, and the resulting bitmap will be scaled from the hardware size.

This new prop is most useful in cases where the destination dimensions are quite small and the source image is significantly larger. The `resize` resize method performs downsampling and significant image quality is lost between the source and destination image sizes, often resulting in a blurry image. By using a multiplier, the decoded image is slightly larger than the target size but smaller than the source image (if the source image is large enough).

It's important to note that Fresco still chooses the closest power of 2 and will not scale the image larger than its source dimensions. If the multiplier yields `ResizeOptions` greater than the source dimensions, no downsampling occurs.

Here's an example: 
If you have a source image with dimensions 200x200 and destination dimensions of 24x24, a `resizeMultiplier` of `2.0` will tell Fresco to downsample the image to 48x48. Fresco picks the closest power of 2 (so, 50x50) and decodes the image into a bitmap of that size. Without the multiplier, the closest power of 2 would be 25x25, which is half the quality.

## Changelog

[Android][Added] - Adds a new `Image` prop `resizeMultiplier` to help increase quality of small images on low DPI devices

Differential Revision: D58120352
  • Loading branch information
Abbondanzo authored and facebook-github-bot committed Jun 5, 2024
1 parent a569c82 commit 01cb923
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 9 deletions.
21 changes: 16 additions & 5 deletions packages/react-native/Libraries/Image/ImageProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ type AndroidImageProps = $ReadOnly<{|
loadingIndicatorSource?: ?(number | $ReadOnly<{|uri: string|}>),
progressiveRenderingEnabled?: ?boolean,
fadeDuration?: ?number,

/**
* The mechanism that should be used to resize the image when the image's
* dimensions differ from the image view's dimensions. Defaults to `'auto'`.
* See https://reactnative.dev/docs/image#resizemethod
*/
resizeMethod?: ?('auto' | 'resize' | 'scale'),

/**
* When the `resizeMethod` is set to `resize`, the destination dimensions are
* multiplied by this value. The `scale` method is used to perform the
* remainder of the resize.
* This is used to produce higher quality images when resizing to small dimensions.
* Defaults to 1.0.
*/
resizeMultiplier?: ?number,
|}>;

export type ImageProps = {|
Expand Down Expand Up @@ -183,11 +199,6 @@ export type ImageProps = {|
*/
onLoadStart?: ?() => void,

/**
* See https://reactnative.dev/docs/image#resizemethod
*/
resizeMethod?: ?('auto' | 'resize' | 'scale'),

/**
* The image source (either a remote URL or a local file resource).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,14 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
validAttributes: {
blurRadius: true,
internal_analyticTag: true,
resizeMethod: true,
resizeMode: true,
resizeMultiplier: true,
tintColor: {
process: require('../StyleSheet/processColor').default,
},
borderBottomLeftRadius: true,
borderTopLeftRadius: true,
resizeMethod: true,
src: true,
// NOTE: New Architecture expects this to be called `source`,
// regardless of the platform, therefore propagate it as well.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4445,6 +4445,8 @@ type AndroidImageProps = $ReadOnly<{|
loadingIndicatorSource?: ?(number | $ReadOnly<{| uri: string |}>),
progressiveRenderingEnabled?: ?boolean,
fadeDuration?: ?number,
resizeMethod?: ?(\\"auto\\" | \\"resize\\" | \\"scale\\"),
resizeMultiplier?: ?number,
|}>;
export type ImageProps = {|
...$Diff<ViewProps, $ReadOnly<{| style: ?ViewStyleProp |}>>,
Expand Down Expand Up @@ -4472,7 +4474,6 @@ export type ImageProps = {|
onLoad?: ?(event: ImageLoadEvent) => void,
onLoadEnd?: ?() => void,
onLoadStart?: ?() => void,
resizeMethod?: ?(\\"auto\\" | \\"resize\\" | \\"scale\\"),
source?: ?ImageSource,
style?: ?ImageStyleProp,
referrerPolicy?: ?(
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6379,6 +6379,7 @@ public class com/facebook/react/views/image/ReactImageManager : com/facebook/rea
public fun setProgressiveRenderingEnabled (Lcom/facebook/react/views/image/ReactImageView;Z)V
public fun setResizeMethod (Lcom/facebook/react/views/image/ReactImageView;Ljava/lang/String;)V
public fun setResizeMode (Lcom/facebook/react/views/image/ReactImageView;Ljava/lang/String;)V
public fun setResizeMultiplier (Lcom/facebook/react/views/image/ReactImageView;F)V
public fun setSource (Lcom/facebook/react/views/image/ReactImageView;Lcom/facebook/react/bridge/ReadableArray;)V
public fun setSrc (Lcom/facebook/react/views/image/ReactImageView;Lcom/facebook/react/bridge/ReadableArray;)V
public fun setTintColor (Lcom/facebook/react/views/image/ReactImageView;Ljava/lang/Integer;)V
Expand Down Expand Up @@ -6412,6 +6413,7 @@ public class com/facebook/react/views/image/ReactImageView : com/facebook/drawee
public fun setOverlayColor (I)V
public fun setProgressiveRenderingEnabled (Z)V
public fun setResizeMethod (Lcom/facebook/react/views/image/ImageResizeMethod;)V
public fun setResizeMultiplier (F)V
public fun setScaleType (Lcom/facebook/drawee/drawable/ScalingUtils$ScaleType;)V
public fun setShouldNotifyLoadEvents (Z)V
public fun setSource (Lcom/facebook/react/bridge/ReadableArray;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ package com.facebook.react.views.image
public enum class ImageResizeMethod {
AUTO,
RESIZE,
SCALE
SCALE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ public void setResizeMethod(ReactImageView view, @Nullable String resizeMethod)
}
}

@ReactProp(name = "resizeMultiplier")
public void setResizeMultiplier(ReactImageView view, float resizeMultiplier) {
if (resizeMultiplier < 0.01f) {
FLog.w(ReactConstants.TAG, "Invalid resize multiplier: '" + resizeMultiplier + "'");
}
view.setResizeMultiplier(resizeMultiplier);
}

@ReactProp(name = "tintColor", customType = "Color")
public void setTintColor(ReactImageView view, @Nullable Integer tintColor) {
if (tintColor == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public CloseableReference<Bitmap> process(Bitmap source, PlatformBitmapFactory b
private int mFadeDurationMs = -1;
private boolean mProgressiveRenderingEnabled;
private ReadableMap mHeaders;
private float mResizeMultiplier = 1.0f;

// We can't specify rounding in XML, so have to do so here
private static GenericDraweeHierarchy buildHierarchy(Context context) {
Expand Down Expand Up @@ -307,6 +308,13 @@ public void setResizeMethod(ImageResizeMethod resizeMethod) {
}
}

public void setResizeMultiplier(float multiplier) {
if (mResizeMultiplier != multiplier) {
mResizeMultiplier = multiplier;
mIsDirty = true;
}
}

public void setSource(@Nullable ReadableArray sources) {
List<ImageSource> tmpSources = new LinkedList<>();

Expand Down Expand Up @@ -478,7 +486,7 @@ public void maybeUpdateView() {
}
Postprocessor postprocessor = MultiPostprocessor.from(postprocessors);

ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null;
ResizeOptions resizeOptions = getResizeOptions(mImageSource);

ImageRequestBuilder imageRequestBuilder =
ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri())
Expand Down Expand Up @@ -601,6 +609,18 @@ private boolean shouldResize(ImageSource imageSource) {
}
}

private ResizeOptions getResizeOptions(ImageSource imageSource) {
if (!shouldResize(imageSource)) {
return null;
}
int width = Math.round((float) getWidth() * mResizeMultiplier);
int height = Math.round((float) getHeight() * mResizeMultiplier);
if (width <= 0 || height <= 0) {
return null;
}
return new ResizeOptions(width, height);
}

private void warnImageSource(String uri) {
// TODO(T189014077): This code-path produces an infinite loop of js calls with logbox.
// This is an issue with Fabric view preallocation, react, and LogBox. Fix.
Expand Down

0 comments on commit 01cb923

Please sign in to comment.