-
-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
imageCaptureProcessing maintain EXIF #2902
Open
melmathari
wants to merge
16
commits into
dimagi:master
Choose a base branch
from
melmathari:2689
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
62ab7b7
imageCaptureProcessing maintain EXIF
melmathari f4f9580
reverted some changes
melmathari 88e9994
remove surpresslint
melmathari 6016f4a
Media tests
melmathari 72aea77
Media Test
melmathari a15c106
FileUtil changes
melmathari 94e6fe9
So yes, it will fail safely if:
melmathari 83b729b
restore classpath and project
melmathari 6ae3f3d
Merge branch 'dimagi:master' into 2689
melmathari f270bcf
Merge branch 'master' into 2689
melmathari 2f5a056
Merge branch 'master' into 2689
melmathari 6bb7fd2
Merge branch 'master' into 2689
melmathari 2cdd7e2
coderabbit review
melmathari a241f23
JavaDocs
melmathari 8a6039e
coderabbit review
melmathari 4bb8c49
coderabbit review
melmathari File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,7 +56,8 @@ private static Pair<File, String> moveAndScaleImage(File originalImage, boolean | |
if (currentWidget != null) { | ||
int maxDimen = currentWidget.getMaxDimen(); | ||
if (maxDimen != -1) { | ||
savedScaledImage = FileUtil.scaleAndSaveImage(originalImage, tempFilePathForScaledImage, maxDimen); | ||
File tempFile = new File(tempFilePathForScaledImage); | ||
savedScaledImage = FileUtil.scaleAndSaveImageWithExif(originalImage, tempFile, maxDimen); | ||
} | ||
} | ||
} | ||
|
@@ -66,17 +67,72 @@ private static Pair<File, String> moveAndScaleImage(File originalImage, boolean | |
// Encrypt the scaled or original image to final path | ||
if (HiddenPreferences.isMediaCaptureEncryptionEnabled()) { | ||
finalFilePath = finalFilePath + MediaWidget.AES_EXTENSION; | ||
File sourceFile = new File(sourcePath); | ||
File destFile = new File(finalFilePath); | ||
|
||
try { | ||
// Extract EXIF data before encryption | ||
ExifInterface sourceExif = null; | ||
String mimeType = FileUtil.getMimeType(sourcePath); | ||
if (mimeType != null && mimeType.startsWith("image/")) { | ||
sourceExif = new ExifInterface(sourcePath); | ||
} | ||
|
||
// Perform encryption | ||
EncryptionIO.encryptFile(sourcePath, finalFilePath, formEntryActivity.getSymetricKey()); | ||
|
||
// If we had EXIF data, store it in a companion file | ||
if (sourceExif != null) { | ||
String exifPath = finalFilePath + ".exif"; | ||
ExifInterface destExif = new ExifInterface(exifPath); | ||
|
||
// Copy all EXIF tags | ||
String[] tagsToPreserve = { | ||
ExifInterface.TAG_GPS_LATITUDE, | ||
ExifInterface.TAG_GPS_LATITUDE_REF, | ||
ExifInterface.TAG_GPS_LONGITUDE, | ||
ExifInterface.TAG_GPS_LONGITUDE_REF, | ||
ExifInterface.TAG_GPS_TIMESTAMP, | ||
ExifInterface.TAG_GPS_DATESTAMP, | ||
ExifInterface.TAG_GPS_ALTITUDE, | ||
ExifInterface.TAG_GPS_ALTITUDE_REF, | ||
ExifInterface.TAG_GPS_AREA_INFORMATION, | ||
ExifInterface.TAG_DATETIME, | ||
ExifInterface.TAG_DATETIME_DIGITIZED, | ||
ExifInterface.TAG_DATETIME_ORIGINAL, | ||
ExifInterface.TAG_OFFSET_TIME, | ||
ExifInterface.TAG_OFFSET_TIME_ORIGINAL, | ||
ExifInterface.TAG_OFFSET_TIME_DIGITIZED, | ||
ExifInterface.TAG_COPYRIGHT, | ||
ExifInterface.TAG_IMAGE_DESCRIPTION, | ||
ExifInterface.TAG_EXIF_VERSION, | ||
ExifInterface.TAG_ORIENTATION | ||
}; | ||
|
||
for (String tag : tagsToPreserve) { | ||
String value = sourceExif.getAttribute(tag); | ||
if (value != null) { | ||
destExif.setAttribute(tag, value); | ||
} | ||
} | ||
Comment on lines
+90
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we reuse code from |
||
destExif.saveAttributes(); | ||
|
||
// Encrypt the EXIF data file as well | ||
EncryptionIO.encryptFile(exifPath, exifPath + MediaWidget.AES_EXTENSION, | ||
formEntryActivity.getSymetricKey()); | ||
// Delete the unencrypted EXIF file | ||
new File(exifPath).delete(); | ||
} | ||
melmathari marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} catch (Exception e) { | ||
throw new IOException("Failed to encrypt " + sourcePath + | ||
" to " + finalFilePath, e); | ||
throw new IOException("Failed to encrypt image and preserve EXIF data from " + | ||
sourcePath + " to " + finalFilePath, e); | ||
} | ||
} else { | ||
try { | ||
FileUtil.copyFile(sourcePath, finalFilePath); | ||
FileUtil.copyFileWithExifData(new File(sourcePath), new File(finalFilePath)); | ||
} catch (Exception e) { | ||
throw new IOException("Failed to rename " + sourcePath + " to " + finalFilePath); | ||
throw new IOException("Failed to copy image with EXIF data from " + | ||
sourcePath + " to " + finalFilePath, e); | ||
} | ||
} | ||
|
||
|
@@ -92,10 +148,10 @@ private static File makeRawCopy(File originalImage, String instanceFolder, Strin | |
} | ||
File rawImageFile = new File(rawDirPath + "/" + imageFilename); | ||
try { | ||
FileUtil.copyFile(originalImage, rawImageFile); | ||
FileUtil.copyFileWithExifData(originalImage, rawImageFile); | ||
} catch (Exception e) { | ||
throw new IOException("Failed to rename " + originalImage.getAbsolutePath() + | ||
" to " + rawImageFile.getAbsolutePath()); | ||
throw new IOException("Failed to copy image with EXIF data from " + | ||
originalImage.getAbsolutePath() + " to " + rawImageFile.getAbsolutePath(), e); | ||
} | ||
return rawImageFile; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
import android.content.Intent; | ||
import android.database.Cursor; | ||
import android.graphics.Bitmap; | ||
import android.media.ExifInterface; | ||
import android.media.MediaMetadataRetriever; | ||
import android.net.Uri; | ||
import android.os.Build; | ||
|
@@ -64,6 +65,12 @@ public class FileUtil { | |
|
||
private static final String LOG_TOKEN = "cc-file-util"; | ||
|
||
/** | ||
* Deletes a file or directory. | ||
* | ||
* @param path The path to the file or directory to delete | ||
* @return true if the file and all of its contents were deleted successfully, false otherwise | ||
*/ | ||
public static boolean deleteFileOrDir(String path) { | ||
return deleteFileOrDir(new File(path)); | ||
} | ||
|
@@ -83,6 +90,13 @@ public static boolean deleteFileOrDir(File f) { | |
return f.delete(); | ||
} | ||
|
||
/** | ||
* Cleans up file paths. | ||
* | ||
* @param fullPath The full path to the file | ||
* @param extendedPath The extended path to stop at | ||
* @return true if the file path was cleaned up successfully, false otherwise | ||
*/ | ||
public static boolean cleanFilePath(String fullPath, String extendedPath) { | ||
//There are actually a few things that can go wrong here, should be careful | ||
|
||
|
@@ -397,7 +411,6 @@ public static void ensureFilePathExists(File f) { | |
* if we are on KitKat we need use the new API to find the mounted roots, then append our application | ||
* specific path that we're allowed to write to | ||
*/ | ||
@SuppressLint("NewApi") | ||
private static String getExternalDirectoryKitKat(Context c) { | ||
File[] extMounts = c.getExternalFilesDirs(null); | ||
// first entry is emualted storage. Second if it exists is secondary (real) SD. | ||
|
@@ -494,8 +507,9 @@ public static String getMd5Hash(File file) { | |
|
||
BigInteger number = new BigInteger(1, messageDigest); | ||
String md5 = number.toString(16); | ||
while (md5.length() < 32) | ||
while (md5.length() < 32) { | ||
md5 = "0" + md5; | ||
} | ||
is.close(); | ||
return md5; | ||
|
||
|
@@ -612,7 +626,8 @@ public static boolean scaleAndSaveImage(File originalImage, String finalFilePath | |
Pair<Bitmap, Boolean> bitmapAndScaledBool = MediaUtil.inflateImageSafe(originalImage.getAbsolutePath()); | ||
if (bitmapAndScaledBool.second) { | ||
Logger.log(LogTypes.TYPE_FORM_ENTRY, | ||
"An image captured during form entry was too large to be processed at its original size, and had to be downsized"); | ||
"An image captured during form entry was too large to be processed at its original size, " + | ||
"and had to be downsized"); | ||
} | ||
Bitmap scaledBitmap = getBitmapScaledByMaxDimen(bitmapAndScaledBool.first, maxDimen); | ||
if (scaledBitmap != null) { | ||
|
@@ -723,7 +738,9 @@ public static Uri getUriForExternalFile(Context context, File file) { | |
* @param dstFile destination File where we need to copy the inputStream | ||
*/ | ||
public static void copyFile(InputStream inputStream, File dstFile) throws IOException { | ||
if (inputStream == null) return; | ||
if (inputStream == null) { | ||
throw new IOException("InputStream is null. Cannot copy file."); | ||
} | ||
OutputStream outputStream = new FileOutputStream(dstFile); | ||
StreamsUtil.writeFromInputToOutputUnmanaged(inputStream, outputStream); | ||
inputStream.close(); | ||
|
@@ -844,17 +861,126 @@ public static void addMediaToGallery(Context context, File file) throws | |
} | ||
|
||
/** | ||
* Returns true only when we're certain that the file size is too large. | ||
* <p> https://developer.android.com/training/secure-file-sharing/retrieve-info.html#RetrieveFileInfo | ||
* Checks if a file referenced by a content URI exceeds the maximum allowed upload size | ||
* <p> | ||
* @param contentResolver ContentResolver to query the file size | ||
* @param uri Content URI of the file to check | ||
* @return true if the file size exceeds FormUploadUtil.MAX_BYTES, false otherwise or if size cannot be determined | ||
* @see FormUploadUtil#MAX_BYTES | ||
* @see <a href="https://developer.android.com/training/secure-file-sharing/retrieve-info.html#RetrieveFileInfo">Android docs</a> | ||
melmathari marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
public static boolean isFileTooLargeToUpload(ContentResolver contentResolver, Uri uri) { | ||
try (Cursor returnCursor = contentResolver.query(uri, null, null, null, null)) { | ||
if (returnCursor == null || returnCursor.getCount() <= 0) { | ||
return false; | ||
} | ||
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE); | ||
returnCursor.moveToFirst(); | ||
return returnCursor.getLong(sizeIndex) > FormUploadUtil.MAX_BYTES; | ||
if (sizeIndex == -1) { | ||
return false; | ||
} | ||
if (returnCursor.moveToFirst()) { | ||
return returnCursor.getLong(sizeIndex) > FormUploadUtil.MAX_BYTES; | ||
} else { | ||
return false; | ||
} | ||
Comment on lines
+878
to
+885
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for making this better. |
||
} | ||
} | ||
|
||
shubham1g5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* Copies a file from source to destination while preserving EXIF metadata for image files. | ||
* For non-image files, performs a regular file copy. | ||
* | ||
* @param sourceFile The source file to copy from | ||
* @param destFile The destination file to copy to | ||
* @throws IOException If there is an error during file copying or EXIF data transfer | ||
*/ | ||
public static void copyFileWithExifData(File sourceFile, File destFile) throws IOException { | ||
// First copy the file normally | ||
copyFile(sourceFile, destFile); | ||
|
||
// Only try to copy EXIF data for image files | ||
String mimeType = getMimeType(sourceFile.getAbsolutePath()); | ||
if (mimeType != null && mimeType.startsWith("image/")) { | ||
try { | ||
copyExifData(sourceFile.getAbsolutePath(), destFile.getAbsolutePath()); | ||
} catch (IOException e) { | ||
throw new IOException("Failed to copy EXIF data", e); | ||
} | ||
} | ||
} | ||
|
||
private static void copyExifData(String sourcePath, String destPath) { | ||
try { | ||
ExifInterface source = new ExifInterface(sourcePath); | ||
ExifInterface dest = new ExifInterface(destPath); | ||
|
||
String[] tagsToPreserve = { | ||
// GPS data | ||
ExifInterface.TAG_GPS_LATITUDE, | ||
ExifInterface.TAG_GPS_LATITUDE_REF, | ||
ExifInterface.TAG_GPS_LONGITUDE, | ||
ExifInterface.TAG_GPS_LONGITUDE_REF, | ||
ExifInterface.TAG_GPS_TIMESTAMP, | ||
ExifInterface.TAG_GPS_DATESTAMP, | ||
ExifInterface.TAG_GPS_ALTITUDE, | ||
ExifInterface.TAG_GPS_ALTITUDE_REF, | ||
ExifInterface.TAG_GPS_AREA_INFORMATION, | ||
|
||
// Timestamp data | ||
ExifInterface.TAG_DATETIME, | ||
ExifInterface.TAG_DATETIME_DIGITIZED, | ||
ExifInterface.TAG_DATETIME_ORIGINAL, | ||
ExifInterface.TAG_OFFSET_TIME, | ||
ExifInterface.TAG_OFFSET_TIME_ORIGINAL, | ||
ExifInterface.TAG_OFFSET_TIME_DIGITIZED, | ||
|
||
// Image metadata | ||
ExifInterface.TAG_COPYRIGHT, | ||
ExifInterface.TAG_IMAGE_DESCRIPTION, | ||
ExifInterface.TAG_EXIF_VERSION, | ||
ExifInterface.TAG_ORIENTATION | ||
}; | ||
|
||
for (String tag : tagsToPreserve) { | ||
String value = source.getAttribute(tag); | ||
if (value != null) { | ||
dest.setAttribute(tag, value); | ||
} | ||
} | ||
|
||
dest.saveAttributes(); | ||
} catch (IOException e) { | ||
// Log but don't fail if EXIF copying fails | ||
Logger.log(LogTypes.TYPE_WARNING_NETWORK, | ||
String.format("Failed to copy EXIF data from %s to %s: %s (Error: %s)", | ||
sourcePath, destPath, e.getMessage(), e.getClass().getSimpleName())); | ||
} | ||
} | ||
|
||
/** | ||
* Scales an image if needed and preserves its EXIF metadata in the process. | ||
* | ||
* @param sourceFile The original image file | ||
* @param destFile The destination file where the scaled image will be saved | ||
* @param maxDimen The maximum dimension (width or height) allowed for the scaled image | ||
* @return true if the operation was successful (either scaled with EXIF preserved or | ||
* copied with EXIF if scaling wasn't needed) | ||
* @throws IOException If there is an error during scaling, file operations or EXIF data transfer | ||
*/ | ||
public static boolean scaleAndSaveImageWithExif(File sourceFile, File destFile, int maxDimen) | ||
throws IOException { | ||
// First scale the image | ||
boolean scaled = scaleAndSaveImage(sourceFile, destFile.getAbsolutePath(), maxDimen); | ||
|
||
if (!scaled) { | ||
// If scaling was not needed, copy the source file to destination with EXIF data | ||
copyFileWithExifData(sourceFile, destFile); | ||
return true; | ||
} else { | ||
// Copy EXIF data from source to scaled image | ||
copyExifData(sourceFile.getAbsolutePath(), destFile.getAbsolutePath()); | ||
} | ||
|
||
return true; | ||
} | ||
melmathari marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am a bit unsure how it's working. A few questions reg. encrypted images -
Curious why do we need to create a separate
.exif
file here to retain exif data ?Before uploading images to the server, we decrypt them first by using a EncryptedFileBody. Do we also need to upload the
.exif
file there to be able to retain exif data on server ?In your testing, have you been able to test the exif retention for final image on server with encyrption enabled ?