Skip to content
Merged
Changes from all commits
Commits
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
63 changes: 58 additions & 5 deletions app/src/org/commcare/utils/FileUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;

import androidx.exifinterface.media.ExifInterface;

import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
Expand Down Expand Up @@ -153,7 +155,7 @@ public static String SanitizeFileName(String input) {
/**
* Copies a source file from a FileProvider Content provider into a file directory local
* to this application.
*
* <p>
* The app needs to already have permissions granted for the external file content.
*
* @param contentUri A valid uri to a contentprovider backed external file
Expand Down Expand Up @@ -318,7 +320,7 @@ public static void copyFileDeep(File oldFolder, File newFolder) throws IOExcepti

/**
* http://stackoverflow.com/questions/11281010/how-can-i-get-external-sd-card-path-for-android-4-0
*
* <p>
* Used in SD Card functionality to get the location of the SD card for reads and writes
* Returns a list of available mounts; for our purposes, we just use the first
*/
Expand Down Expand Up @@ -620,7 +622,7 @@ private static String last(String[] strings) {

private static void copyExifData(ExifInterface sourceExif, ExifInterface destExif, Bitmap scaledBitmap) {
if (sourceExif == null || destExif == null) {
return;
return;
}
for (String tag : EXIF_TAGS) {
String value = sourceExif.getAttribute(tag);
Expand Down Expand Up @@ -698,6 +700,52 @@ public static void writeBitmapToDiskAndCleanupHandles(Bitmap bitmap, ImageType t
}
}

/**
* Progressively scales down a bitmap to avoid moiré patterns by using a step-wise approach.
* This method first performs progressive halving until the dimensions are close to the target,
* then completes the scaling with a final resize to exactly match the target dimensions.
* The step-wise approach reduces aliasing artifacts that would occur with direct scaling,
* particularly important for images with fine patterns or textures.
*
* @param originalBitmap The source bitmap to downscale
* @param targetWidth The desired width of the resulting bitmap
* @param targetHeight The desired height of the resulting bitmap
* @return A downscaled bitmap that matches the target dimensions
*/
private static Bitmap stepDownscale(Bitmap originalBitmap, int targetWidth, int targetHeight) {
Bitmap currentBitmap = originalBitmap;
int height = originalBitmap.getHeight();
int width = originalBitmap.getWidth();

// First do progressive halving until we get close
while (height > targetHeight * 2 && width > targetWidth * 2) {
height /= 2;
width /= 2;

Bitmap tempBitmap = Bitmap.createScaledBitmap(currentBitmap, width, height, true);

if (currentBitmap != originalBitmap && !currentBitmap.isRecycled()) {
currentBitmap.recycle();
}

currentBitmap = tempBitmap;
}

// Final step to exactly match target dimensions
if (width != targetWidth || height != targetHeight) {
Bitmap finalBitmap = Bitmap.createScaledBitmap(currentBitmap, targetWidth, targetHeight, false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be curious on your reasoning of putting bilinear filtering as false here and true above while wondering if it should be false even above or true even here ?

Copy link
Contributor Author

@rahulrajesh21 rahulrajesh21 May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shubham1g5 Bilinear filtering is set as true to prevent visual artifacts at the cost of image quality. For the final step it is kept false to persevere image quality and reduce performance impact.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate more on what do you mean by preventing visual artifacts and why does it not matter in the final step when it matters in the intermediary steps ?

Copy link
Contributor Author

@rahulrajesh21 rahulrajesh21 May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shubham1g5 Visual artifacts like moiré patterns and stippling occur primarily when downsampling at large ratios. Bilinear filtering averages neighboring pixels, which helps smooth out these artifacts.
When reducing dimensions by large factors, the sampling grid can align improperly with high-frequency details in the original image. This causes interference patterns - similar to strange patterns in digital photos of striped fabrics. In intermediary steps, bilinear filtering helps by averaging pixels, softening these problematic interference patterns.

In the final step, these artifacts matter less because:

  1. The scaling ratio is much smaller.
  2. The major frequency mismatches that cause interference have already been addressed in previous steps.
  3. When sampling distance is smaller, the likelihood of problematic frequency interactions is reduced.

In the final step, bilinear filtering could also be kept true, but may reduce image quality and impact performance. So it is kept false to preserve sharpness and detail in the final image while maintaining better performance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's insightful, thanks for the detailed explanation here.


if (currentBitmap != originalBitmap && !currentBitmap.isRecycled()) {
currentBitmap.recycle();
}

return finalBitmap;
}

return currentBitmap;
}


/**
* Attempts to scale down an image file based on the max dimension given, using the following
* logic: If at least one of the dimensions of the original image exceeds the max dimension
Expand All @@ -721,17 +769,22 @@ private static Bitmap getBitmapScaledByMaxDimen(Bitmap originalBitmap, int maxDi
double aspectRatio = ((double)otherSide) / sideToScale;
sideToScale = maxDimen;
otherSide = (int)Math.floor(maxDimen * aspectRatio);
int targetWidth, targetHeight;
if (width > height) {
// if width was the side that got scaled
return Bitmap.createScaledBitmap(originalBitmap, sideToScale, otherSide, false);
targetWidth = sideToScale;
targetHeight = otherSide;
} else {
return Bitmap.createScaledBitmap(originalBitmap, otherSide, sideToScale, false);
targetWidth = otherSide;
targetHeight = sideToScale;
}
return stepDownscale(originalBitmap, targetWidth, targetHeight);
} else {
return null;
}
}


public static boolean isFileOversized(File mf) {
double length = getFileSize(mf);
return length > WARNING_SIZE;
Expand Down