Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
15287e3
Rescpect image orientation while previewing the image in mobile app
hashamyounis9 Apr 10, 2025
7d4e597
use appropriate log tags, set proper exceptions
hashamyounis9 Apr 10, 2025
198a5fc
use Logger.exception()
hashamyounis9 Apr 11, 2025
154c5ab
avoid NullPointerException gracefully
hashamyounis9 Apr 11, 2025
3510e9d
Merge branch 'master' into respect-image-orientation-while-previewing…
hashamyounis9 Apr 11, 2025
d13efcb
Merge branch 'master' into respect-image-orientation-while-previewing…
hashamyounis9 Apr 11, 2025
cb79487
attempt avoid NullPointerException: return bitmap with expected width…
hashamyounis9 Apr 12, 2025
d5091cc
attempt avoid NullPointerException: avoid extra call to inflateImageSafe
hashamyounis9 Apr 12, 2025
8c5ca49
attempt avoid NullPointerException: return the original bitmap, rotat…
hashamyounis9 Apr 12, 2025
cde16e3
attempt avoid NullPointerException: set appropriate checks
hashamyounis9 Apr 12, 2025
af5b9aa
attempt avoid NullPointerException: update image to have EXIF properties
hashamyounis9 Apr 14, 2025
ccc7959
attempt avoid NullPointerException: try changing image
hashamyounis9 Apr 15, 2025
c50f151
attempt avoid NullPointerException: do not read exif not pngs
hashamyounis9 Apr 17, 2025
f56dd0b
Merge branch 'master' into respect-image-orientation-while-previewing…
hashamyounis9 Apr 17, 2025
36d60f0
read orientation exif and rotate, continue otherwose by avoiding bitm…
hashamyounis9 Apr 19, 2025
5562446
set proper exception checks
hashamyounis9 Apr 20, 2025
ddf1847
set proper exception checks
hashamyounis9 Apr 21, 2025
b6a5605
Merge branch 'master' into respect-image-orientation-while-previewing…
hashamyounis9 Apr 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 87 additions & 27 deletions app/src/org/commcare/utils/MediaUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.AudioManager;
import androidx.exifinterface.media.ExifInterface;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
Expand Down Expand Up @@ -79,7 +81,7 @@ public static Bitmap inflateDisplayImage(Context context, String jrUri,
}

DisplayMetrics displayMetrics = new DisplayMetrics();
((WindowManager)context.getSystemService(Context.WINDOW_SERVICE))
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getMetrics(displayMetrics);

Expand Down Expand Up @@ -109,7 +111,7 @@ public static Bitmap inflateDisplayImage(Context context, String jrUri,

public static int getActionBarHeightInPixels(Context context) {
final TypedArray styledAttributes = context.getTheme().obtainStyledAttributes(
new int[] { android.R.attr.actionBarSize });
new int[]{android.R.attr.actionBarSize});
int actionBarSize = (int) styledAttributes.getDimension(0, 0);
styledAttributes.recycle();

Expand All @@ -128,7 +130,7 @@ public static Bitmap inflateDisplayImage(Context context, String jrUri) {
/**
* Attempt to inflate an image source into a bitmap whose final dimensions are based upon
* 2 factors:
*
* <p>
* 1) The application of a scaling factor, which is derived from the relative values of the
* target density declared by the app and the current device's actual density
* 2) The absolute dimensions of the bounding container into which this image is being inflated
Expand All @@ -154,8 +156,8 @@ public static Bitmap getBitmapScaledForNativeDensity(DisplayMetrics metrics, Str
int imageWidth = o.outWidth;

double scaleFactor = computeInflationScaleFactor(metrics, targetDensity);
int calculatedHeight = Math.round((float)(imageHeight * scaleFactor));
int calculatedWidth = Math.round((float)(imageWidth * scaleFactor));
int calculatedHeight = Math.round((float) (imageHeight * scaleFactor));
int calculatedWidth = Math.round((float) (imageWidth * scaleFactor));

Bitmap toReturn;

Expand Down Expand Up @@ -188,10 +190,10 @@ private static void attemptWriteCacheToLocation(Bitmap toReturn, File cacheLocat
/**
* Attempts to load a cached filepath from the given location and tag, and returns the
* location for the cached file either way.
*
* <p>
* If caching is unavailable, null should be returned. If an object is returned, the first
* argument must be non-null, and must have the same extension as the input filepath.
*
* <p>
* The cache key/object will handle its own file path/modified clearance, the tag provided
* should differentiate between different ways of inflating the provided image path
*/
Expand All @@ -208,13 +210,13 @@ private static Pair<File, Bitmap> getCacheFileLocationAndBitmap(String imageFile
} catch (RuntimeException e) {
try {
cacheKey.delete();
Log.d(TAG, "Removed potentially invalid cache from " +cacheKey.toString());
Log.d(TAG, "Removed potentially invalid cache from " + cacheKey.toString());
} catch (Exception inner) {

}
}
}
return new Pair<>(cacheKey,b);
return new Pair<>(cacheKey, b);
}

private static File getCacheFileLocation(String imageFilepath, String tag) {
Expand Down Expand Up @@ -264,7 +266,7 @@ public static String getHashedImageFilepath(String input) {
*/
public static double computeInflationScaleFactor(DisplayMetrics metrics, int targetDensity) {
final int SCREEN_DENSITY = metrics.densityDpi;
double customDpiScaleFactor = (double)SCREEN_DENSITY / targetDensity;
double customDpiScaleFactor = (double) SCREEN_DENSITY / targetDensity;
double proportionalAdjustmentFactor = getCustomAndroidAdjustmentFactor(metrics);
return customDpiScaleFactor * proportionalAdjustmentFactor;
}
Expand All @@ -276,7 +278,7 @@ private static double getCustomAndroidAdjustmentFactor(DisplayMetrics metrics) {
// Android is taking other factors into consideration (such as straight up screen size)
// when it re-sizes an image for this device, so we want to incorporate that proportionally
// into our own version of the scale factor
double standardNativeScaleFactor = (double)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
double standardNativeScaleFactor = (double) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;

double actualNativeScaleFactor = metrics.density;
if (actualNativeScaleFactor > standardNativeScaleFactor) {
Expand All @@ -292,19 +294,19 @@ private static double getCustomAndroidAdjustmentFactor(DisplayMetrics metrics) {
/**
* @return A bitmap representation of the given image file, scaled down to the smallest
* size that still fills the container
*
* <p>
* More precisely, preserves the following 2 conditions:
* 1. The larger of the 2 sides takes on the size of the corresponding container dimension
* (e.g. if its width is larger than its height, then the new width should = containerWidth)
* 2. The aspect ratio of the original image is maintained (so the height would get scaled
* down proportionally with the width)
*/
public static Bitmap getBitmapScaledToContainer(String imageFilepath, int containerHeight,
int containerWidth,
int containerWidth,
boolean respectBoundsExactly) {

Pair<File, Bitmap> cacheKey = getCacheFileLocationAndBitmap(imageFilepath,
String.format("container_%d_%d_%b",containerHeight, containerWidth,
String.format("container_%d_%d_%b", containerHeight, containerWidth,
respectBoundsExactly));

if (cacheKey != null && cacheKey.second != null) {
Expand Down Expand Up @@ -341,7 +343,6 @@ public static Bitmap getBitmapScaledToContainer(File imageFile, int containerHei
* of an exact size based on a target width and height. In this
* case, targetWidth and targetHeight are ignored and the 2nd case
* below is used.
*
* @return A bitmap representation of the given image file, scaled down if necessary such that
* the new dimensions of the image are the SMALLER of the following 2 options:
* 1) targetHeight and targetWidth
Expand Down Expand Up @@ -370,21 +371,80 @@ private static Bitmap scaleDownToTargetOrContainer(String imageFilepath,
int approximateScaleDownFactor = getApproxScaleDownFactor(newWidth, originalWidth);
Bitmap b = inflateImageSafe(imageFilepath, approximateScaleDownFactor).first;

int orientation = ExifInterface.ORIENTATION_NORMAL;
try {
ExifInterface exif = new ExifInterface(imageFilepath);
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
} catch (Exception e) {
Logger.exception("Unable to read exif data from image file: ", e);
}

// Rotate the bitmap if needed
Bitmap rotatedBitmap = b;
if (orientation != ExifInterface.ORIENTATION_NORMAL) {
rotatedBitmap = rotateBitmap(b, orientation);
}

if (scaleByContainerOnly && !respectBoundsExactly) {
// Not worth performance loss of creating an exact scaled bitmap in this case
return b;
return rotatedBitmap;
} else {
try {
// Here we want to be more precise because we have a target width and height, or
// specified that respecting the bounding container precisely is important
return Bitmap.createScaledBitmap(b, newWidth, newHeight, false);
return Bitmap.createScaledBitmap(rotatedBitmap, newWidth, newHeight, false);
} catch (OutOfMemoryError e) {
Log.d(TAG, "Ran out of memory attempting to scale image at: " + imageFilepath);
Logger.exception("Ran out of memory attempting to scale image at: " + imageFilepath + " with exception: ", e);
rotatedBitmap.recycle();
return null;
}
}
}

// Helper method to rotate the bitmap based on EXIF orientation that we previously retained
private static Bitmap rotateBitmap(Bitmap bitmap, int orientation) {
if (bitmap == null) {
return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
if (orientation == ExifInterface.ORIENTATION_NORMAL) {
return bitmap;
}

Matrix matrix = new Matrix();
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
matrix.postRotate(90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
matrix.postRotate(180);
break;
case ExifInterface.ORIENTATION_ROTATE_270:
matrix.postRotate(270);
break;
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
matrix.postScale(1, -1);
break;
case ExifInterface.ORIENTATION_TRANSPOSE:
matrix.postRotate(90);
matrix.postScale(-1, 1);
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
matrix.postRotate(270);
matrix.postScale(-1, 1);
break;
default:
return bitmap;
}
try {
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
} catch (OutOfMemoryError e) {
Logger.exception("Ran out of memory attempting to rotate bitmap", e);
return bitmap;
}
}

private static int getApproxScaleDownFactor(int newWidth, int originalWidth) {
if (newWidth == 0) {
Expand Down Expand Up @@ -414,14 +474,14 @@ private static Pair<Integer, Integer> getRoughDimensImposedByContainer(int origi
return new Pair<>(originalWidth, originalHeight);
}

double heightScaleDownFactor = (double)boundingHeight / originalHeight;
double widthScaleDownFactor = (double)boundingWidth / originalWidth;
double heightScaleDownFactor = (double) boundingHeight / originalHeight;
double widthScaleDownFactor = (double) boundingWidth / originalWidth;
// Choosing the larger of the scale down factors, so that the image still fills the entire
// container
double dominantScaleDownFactor = Math.max(widthScaleDownFactor, heightScaleDownFactor);

int widthImposedByContainer = (int)Math.round(originalWidth * dominantScaleDownFactor);
int heightImposedByContainer = (int)Math.round(originalHeight * dominantScaleDownFactor);
int widthImposedByContainer = (int) Math.round(originalWidth * dominantScaleDownFactor);
int heightImposedByContainer = (int) Math.round(originalHeight * dominantScaleDownFactor);
return new Pair<>(widthImposedByContainer, heightImposedByContainer);

}
Expand Down Expand Up @@ -464,12 +524,12 @@ private static Pair<Integer, Integer> boundedScaleUpHelper(int originalHeight,
int originalWidth,
int boundingHeight,
int boundingWidth) {
double heightScaleFactor = (double)boundingHeight / originalHeight;
double widthScaleFactor = (double)boundingWidth / originalWidth;
double heightScaleFactor = (double) boundingHeight / originalHeight;
double widthScaleFactor = (double) boundingWidth / originalWidth;
double dominantScaleFactor = Math.min(heightScaleFactor, widthScaleFactor);

int scaledUpWidthImposedByContainer = (int)Math.round(originalWidth * dominantScaleFactor);
int scaledUpHeightImposedByContainer = (int)Math.round(originalHeight * dominantScaleFactor);
int scaledUpWidthImposedByContainer = (int) Math.round(originalWidth * dominantScaleFactor);
int scaledUpHeightImposedByContainer = (int) Math.round(originalHeight * dominantScaleFactor);
return new Pair<>(scaledUpWidthImposedByContainer, scaledUpHeightImposedByContainer);
}

Expand Down Expand Up @@ -508,7 +568,7 @@ private static Pair<Bitmap, Boolean> performSafeScaleDown(String imageFilepath,
}

@RequiresApi(api = Build.VERSION_CODES.N)
public static boolean isRecordingActive(Context context){
public static boolean isRecordingActive(Context context) {
return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
.getActiveRecordingConfigurations().size() > 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

import java.io.File;
import java.net.URL;

@Config(application = CommCareTestApplication.class)
@RunWith(AndroidJUnit4.class)
public class ImageInflationTest {
Expand All @@ -30,7 +33,7 @@ public class ImageInflationTest {
private DisplayMetrics createFakeDisplayMetrics(int screenDensity) {
DisplayMetrics metrics = new DisplayMetrics();
metrics.densityDpi = screenDensity;
metrics.density = (float)screenDensity / DisplayMetrics.DENSITY_DEFAULT;
metrics.density = (float) screenDensity / DisplayMetrics.DENSITY_DEFAULT;
return metrics;
}

Expand All @@ -53,7 +56,14 @@ private void testCorrectInflationWithDensity(int targetDensity, DisplayMetrics m

@Before
public void init() {
imageFilepath = "/images/100x100.png";
URL resource = getClass().getClassLoader().getResource("images/100x100.png");
if (resource == null) {
throw new IllegalStateException("Test resource not found: images/100x100.png");
}

File file = new File(resource.getFile());
imageFilepath = file.getAbsolutePath();

lowDensityDevice = createFakeDisplayMetrics(DisplayMetrics.DENSITY_LOW);
mediumDensityDevice = createFakeDisplayMetrics(DisplayMetrics.DENSITY_MEDIUM);
highDensityDevice = createFakeDisplayMetrics(DisplayMetrics.DENSITY_HIGH);
Expand All @@ -62,7 +72,7 @@ public void init() {

@Test
public void testScaleFactorComputationSimple() {
Assert.assertEquals((double)160 / 280,
Assert.assertEquals((double) 160 / 280,
MediaUtil.computeInflationScaleFactor(mediumDensityDevice, DisplayMetrics.DENSITY_280), .1);
}

Expand All @@ -72,16 +82,16 @@ public void testScaleFactorComputationSimple() {
@Test
public void testScaleFactorComputationComplex1() {
// the expected value of density for a 160 dpi device is 160/160 = 1, so this is 10% larger
mediumDensityDevice.density = (float)1.1;
Assert.assertEquals((double)160 / 280 * 1.1,
mediumDensityDevice.density = (float) 1.1;
Assert.assertEquals((double) 160 / 280 * 1.1,
MediaUtil.computeInflationScaleFactor(mediumDensityDevice, DisplayMetrics.DENSITY_280), .1);
}

@Test
public void testScaleFactorComputationComplex2() {
// the expected value of density for 120dpi device is 120/160 = .75, so this is 33% smaller
lowDensityDevice.density = (float)0.5;
Assert.assertEquals((double)120 / 280 * ((double)2 / 3),
lowDensityDevice.density = (float) 0.5;
Assert.assertEquals((double) 120 / 280 * ((double) 2 / 3),
MediaUtil.computeInflationScaleFactor(lowDensityDevice, DisplayMetrics.DENSITY_280), .1);
}

Expand Down