Skip to content

Commit

Permalink
chore: increase the image size limit to 100MB
Browse files Browse the repository at this point in the history
  • Loading branch information
criticalAY committed Sep 24, 2024
1 parent d912113 commit c6e4935
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.ichi2.anki.multimedia
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
Expand All @@ -42,13 +43,18 @@ import com.ichi2.anki.R
import com.ichi2.anki.multimedia.MultimediaActivity.Companion.EXTRA_MEDIA_OPTIONS
import com.ichi2.anki.multimedia.MultimediaActivity.Companion.MULTIMEDIA_RESULT
import com.ichi2.anki.multimedia.MultimediaActivity.Companion.MULTIMEDIA_RESULT_FIELD_INDEX
import com.ichi2.anki.multimedia.MultimediaUtils.IMAGE_LIMIT
import com.ichi2.anki.multimedia.MultimediaUtils.IMAGE_SAVE_MAX_WIDTH
import com.ichi2.anki.multimedia.MultimediaUtils.createCachedFile
import com.ichi2.anki.multimedia.MultimediaUtils.createImageFile
import com.ichi2.anki.multimedia.MultimediaUtils.createNewCacheImageFile
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.annotations.NeedsTest
import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat
import com.ichi2.imagecropper.ImageCropper
import com.ichi2.imagecropper.ImageCropper.Companion.CROP_IMAGE_RESULT
import com.ichi2.utils.BitmapUtil
import com.ichi2.utils.ExifUtil
import com.ichi2.utils.FileUtil
import com.ichi2.utils.message
import com.ichi2.utils.negativeButton
Expand All @@ -58,7 +64,10 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.text.DecimalFormat

@NeedsTest("Ensure correct option is executed i.e. gallery or camera")
class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_image) {
Expand Down Expand Up @@ -300,6 +309,10 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
Timber.d("Image length is not valid")
return@setOnClickListener
}
if (viewModel.selectedMediaFileSize > IMAGE_LIMIT) {
showLargeFileCropDialog((1.0 * viewModel.selectedMediaFileSize / IMAGE_LIMIT).toFloat())
return@setOnClickListener
}

field.mediaPath = viewModel.currentMultimediaPath.value
field.hasTemporaryMedia = true
Expand Down Expand Up @@ -392,6 +405,30 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
imageFileSize.text = file.toHumanReadableSize()
}

private fun showLargeFileCropDialog(length: Float) {
val decimalFormat = DecimalFormat(".00")
val size = decimalFormat.format(length.toDouble())
val message = getString(R.string.save_dialog_content, size)
showCompressImageDialog(message)
}

private fun showCompressImageDialog(message: String) {
AlertDialog.Builder(requireActivity()).show {
message(text = message)
positiveButton(R.string.compress) {
viewModel.currentMultimediaPath.value.let {
if (it == null) return@positiveButton
if (!rotateAndCompress(it)) {
Timber.d("Unable to compress the clicked image")
showErrorDialog(errorMessage = resources.getString(R.string.multimedia_editor_image_compression_failed))
return@positiveButton
}
}
}
negativeButton(R.string.dialog_no)
}
}

private fun showCropDialog(message: String) {
if (viewModel.currentMultimediaUri.value == null) {
Timber.w("showCropDialog called with null URI or Path")
Expand Down Expand Up @@ -469,6 +506,68 @@ class MultimediaImageFragment : MultimediaFragment(R.layout.fragment_multimedia_
}
}

/**
* Rotate and compress the image, with the side effect of the current image being backed by a new file
*
* @return true if successful, false indicates the current image is likely not usable, revert if possible
*/
private fun rotateAndCompress(imagePath: String): Boolean {
Timber.d("rotateAndCompress() on %s", imagePath)

// Set the rotation of the camera image and save as JPEG
val imageFile = File(imagePath)
Timber.d("rotateAndCompress in path %s has size %d", imageFile.absolutePath, imageFile.length())

// Load into a bitmap with max size of 1920 pixels and rotate if necessary
var bitmap = BitmapUtil.decodeFile(imageFile, IMAGE_SAVE_MAX_WIDTH)
if (bitmap == null) {
Timber.w("rotateAndCompress() unable to decode file %s", imagePath)
return false
}

var out: FileOutputStream? = null
try {
val outFile = createNewCacheImageFile(directory = ankiCacheDirectory)
out = FileOutputStream(outFile)

// Rotate the bitmap if needed
bitmap = ExifUtil.rotateFromCamera(imageFile, bitmap)

// Compress the bitmap to JPEG format with 90% quality
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)

// Delete the original image file
if (!imageFile.delete()) {
Timber.w("rotateAndCompress() delete of pre-compressed image failed %s", imagePath)
}

val imageUri = getUriForFile(outFile)

// TODO: see if we can use one value to the viewModel
viewModel.updateCurrentMultimediaUri(imageUri)
viewModel.updateCurrentMultimediaPath(outFile.path)
imagePreview.setImageURI(imageUri)
viewModel.selectedMediaFileSize = outFile.length()
updateAndDisplayImageSize(outFile.path)

Timber.d("rotateAndCompress out path %s has size %d", outFile.absolutePath, outFile.length())
} catch (e: FileNotFoundException) {
Timber.w(e, "rotateAndCompress() File not found for image compression %s", imagePath)
return false
} catch (e: IOException) {
Timber.w(e, "rotateAndCompress() create file failed for file %s", imagePath)
return false
} finally {
try {
out?.close()
} catch (e: IOException) {
Timber.w(e, "rotateAndCompress() Unable to clean up image compression output stream")
}
}

return true
}

private fun internalizeUri(uri: Uri): File? {
val internalFile: File
val uriFileName = MultimediaUtils.getImageNameFromUri(requireContext(), uri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ import java.io.File
import java.io.IOException

object MultimediaUtils {
/**
* Creates a new temporary image file in the specified cache directory.
*
* @param extension The desired file extension (default: "jpg").
* @return The newly created image file.
* @throws IOException If an error occurs while creating the file.
*/
@Throws(IOException::class)
fun createNewCacheImageFile(extension: String = "jpg", directory: String?): File {
val storageDir = File(directory!!)
return File.createTempFile("img", ".$extension", storageDir)
}

const val IMAGE_SAVE_MAX_WIDTH = 1920

/**
* https://cs.android.com/android/platform/superproject/+/master:packages/providers/DownloadProvider/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java;l=24
*/
Expand All @@ -45,6 +60,8 @@ object MultimediaUtils {
*/
private const val RAW_DOCUMENTS_FILE_PREFIX = "raw:"

const val IMAGE_LIMIT = 1024 * 1024 * 100 // 1MB in bytes

/**
* Get image name based on uri and selection args
*
Expand Down
1 change: 1 addition & 0 deletions AnkiDroid/src/main/res/values/02-strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
<string name="apk_share_error">Error sharing apkg file</string>
<!-- Import and export v2 feedback -->
<string name="activity_start_failed">The system does not have an app installed that can perform this action.</string>
<string name="multimedia_editor_image_compression_failed"><![CDATA[Camera images may be large. You may wish to compress & resize images in the media directory]]></string>
<string name="no_browser_msg">No browser found for opening the link: %s</string>
<string name="web_page_error">Error loading page: %s</string>
<!-- The name of the deck which corrupt cards will be moved to -->
Expand Down
1 change: 1 addition & 0 deletions AnkiDroid/src/main/res/values/16-multimedia-editor.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<!-- Crop function-->
<string name="crop_image">Do you want to crop this image?</string>
<string name="crop_button">Crop image</string>
<string name="save_dialog_content">Current image size is %sMB. Default image size limit is 1MB. Do you want to compress it?</string>
<string name="select_image_failed">"Select image failed, Please retry"</string>
<string name="activity_result_unexpected">App returned an unexpected value. You may need to use a different app</string>
<string name="audio_recording_field_list" comment="Displays a list of the field data in the note editor while audio recording is taking place">Field Contents</string>
Expand Down

0 comments on commit c6e4935

Please sign in to comment.