Add option to bulk resize images

This commit is contained in:
Naveen 2023-05-24 02:19:29 +05:30
parent 945f8be31c
commit 428a157f7f
6 changed files with 354 additions and 30 deletions

View file

@ -16,7 +16,6 @@ import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -47,7 +46,6 @@ import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.adapters.MyPagerAdapter import com.simplemobiletools.gallery.pro.adapters.MyPagerAdapter
import com.simplemobiletools.gallery.pro.asynctasks.GetMediaAsynctask import com.simplemobiletools.gallery.pro.asynctasks.GetMediaAsynctask
import com.simplemobiletools.gallery.pro.dialogs.DeleteWithRememberDialog import com.simplemobiletools.gallery.pro.dialogs.DeleteWithRememberDialog
import com.simplemobiletools.gallery.pro.dialogs.ResizeWithPathDialog
import com.simplemobiletools.gallery.pro.dialogs.SaveAsDialog import com.simplemobiletools.gallery.pro.dialogs.SaveAsDialog
import com.simplemobiletools.gallery.pro.dialogs.SlideshowDialog import com.simplemobiletools.gallery.pro.dialogs.SlideshowDialog
import com.simplemobiletools.gallery.pro.extensions.* import com.simplemobiletools.gallery.pro.extensions.*
@ -1050,34 +1048,7 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)
private fun resizeImage() { private fun resizeImage() {
val oldPath = getCurrentPath() val oldPath = getCurrentPath()
val originalSize = oldPath.getImageResolution(this) ?: return launchResizeImageDialog(oldPath)
ResizeWithPathDialog(this, originalSize, oldPath) { newSize, newPath ->
ensureBackgroundThread {
try {
var oldExif: ExifInterface? = null
if (isNougatPlus()) {
val inputStream = contentResolver.openInputStream(Uri.fromFile(File(oldPath)))
oldExif = ExifInterface(inputStream!!)
}
val newBitmap = Glide.with(applicationContext).asBitmap().load(oldPath).submit(newSize.x, newSize.y).get()
val newFile = File(newPath)
val newFileDirItem = FileDirItem(newPath, newPath.getFilenameFromPath())
getFileOutputStream(newFileDirItem, true) {
if (it != null) {
saveBitmap(newFile, newBitmap, it, oldExif, File(oldPath).lastModified())
} else {
toast(R.string.image_editing_failed)
}
}
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
} catch (e: Exception) {
showErrorToast(e)
}
}
}
} }
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)

View file

@ -144,6 +144,7 @@ class MediaAdapter(
findItem(R.id.cab_open_with).isVisible = isOneItemSelected findItem(R.id.cab_open_with).isVisible = isOneItemSelected
findItem(R.id.cab_edit).isVisible = isOneItemSelected findItem(R.id.cab_edit).isVisible = isOneItemSelected
findItem(R.id.cab_set_as).isVisible = isOneItemSelected findItem(R.id.cab_set_as).isVisible = isOneItemSelected
findItem(R.id.cab_resize).isVisible = selectedItems.all { it.isImage() }
findItem(R.id.cab_confirm_selection).isVisible = isAGetIntent && allowMultiplePicks && selectedKeys.isNotEmpty() findItem(R.id.cab_confirm_selection).isVisible = isAGetIntent && allowMultiplePicks && selectedKeys.isNotEmpty()
findItem(R.id.cab_restore_recycle_bin_files).isVisible = selectedPaths.all { it.startsWith(activity.recycleBinPath) } findItem(R.id.cab_restore_recycle_bin_files).isVisible = selectedPaths.all { it.startsWith(activity.recycleBinPath) }
findItem(R.id.cab_create_shortcut).isVisible = isOreoPlus() && isOneItemSelected findItem(R.id.cab_create_shortcut).isVisible = isOreoPlus() && isOneItemSelected
@ -179,6 +180,7 @@ class MediaAdapter(
R.id.cab_open_with -> openPath() R.id.cab_open_with -> openPath()
R.id.cab_fix_date_taken -> fixDateTaken() R.id.cab_fix_date_taken -> fixDateTaken()
R.id.cab_set_as -> setAs() R.id.cab_set_as -> setAs()
R.id.cab_resize -> resize()
R.id.cab_delete -> checkDeleteConfirmation() R.id.cab_delete -> checkDeleteConfirmation()
} }
} }
@ -286,6 +288,20 @@ class MediaAdapter(
activity.setAs(path) activity.setAs(path)
} }
private fun resize() {
val paths = getSelectedPaths()
if (isOneItemSelected()) {
val path = paths.first()
activity.launchResizeImageDialog(path) {
finishActMode()
}
} else {
activity.launchResizeMultipleImagesDialog(paths) {
finishActMode()
}
}
}
private fun toggleFileVisibility(hide: Boolean) { private fun toggleFileVisibility(hide: Boolean) {
ensureBackgroundThread { ensureBackgroundThread {
getSelectedItems().forEach { getSelectedItems().forEach {

View file

@ -0,0 +1,160 @@
package com.simplemobiletools.gallery.pro.dialogs
import android.graphics.Point
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.widget.doAfterTextChanged
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.gallery.pro.R
import com.simplemobiletools.gallery.pro.extensions.config
import com.simplemobiletools.gallery.pro.extensions.ensureWriteAccess
import com.simplemobiletools.gallery.pro.extensions.fixDateTaken
import com.simplemobiletools.gallery.pro.extensions.resizeImage
import kotlinx.android.synthetic.main.dialog_resize_multiple_images.view.resize_factor_edit_text
import kotlinx.android.synthetic.main.dialog_resize_multiple_images.view.resize_factor_info
import kotlinx.android.synthetic.main.dialog_resize_multiple_images.view.resize_factor_input_layout
import kotlinx.android.synthetic.main.dialog_resize_multiple_images.view.resize_progress
import java.io.File
import kotlin.math.roundToInt
private const val DEFAULT_RESIZE_FACTOR = "75"
private const val RESIZE_FACTOR_ERROR_DELAY = 800L
class ResizeMultipleImagesDialog(
private val activity: BaseSimpleActivity,
private val imagePaths: List<String>,
private val imageSizes: List<Point>,
private val callback: () -> Unit
) {
private var dialog: AlertDialog? = null
private val view = activity.layoutInflater.inflate(R.layout.dialog_resize_multiple_images, null)
private val progressView = view.resize_progress
private val resizeFactorEditText = view.resize_factor_edit_text
init {
setupViews(view)
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.resize_multiple_images) { alertDialog ->
dialog = alertDialog
alertDialog.showKeyboard(resizeFactorEditText)
val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)
positiveButton.setOnClickListener {
val resizeFactorText = resizeFactorEditText.text?.toString()
val resizeFactor = try {
resizeFactorText?.toFloat()?.div(100)
} catch (e: Exception) {
null
}
if (resizeFactor == null) {
activity.toast(R.string.resize_factor_error)
return@setOnClickListener
}
alertDialog.setCanceledOnTouchOutside(false)
arrayOf(view.resize_factor_input_layout, view.resize_factor_info, positiveButton, negativeButton).forEach {
it.isEnabled = false
it.alpha = 0.6f
}
resizeImages(resizeFactor)
}
}
}
}
private fun resizeImages(factor: Float) {
progressView.show()
ensureBackgroundThread {
with(activity) {
val newSizes = imageSizes.map {
val width = (it.x * factor).roundToInt()
val height = (it.y * factor).roundToInt()
Point(width, height)
}
val parentPath = imagePaths.first().getParentPath()
val pathsToRescan = arrayListOf<String>()
ensureWriteAccess(parentPath) {
for (i in imagePaths.indices) {
val path = imagePaths[i]
val size = newSizes[i]
try {
resizeImage(path, size) {
if (it) {
pathsToRescan.add(path)
runOnUiThread {
progressView.progress = i + 1
}
}
}
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
} catch (e: Exception) {
showErrorToast(e)
}
}
val failureCount = imagePaths.size - pathsToRescan.size
if (failureCount > 0) {
toast(getString(R.string.failed_to_resize_images, failureCount))
} else {
toast(R.string.images_resized_successfully)
}
rescanPaths(pathsToRescan) {
fixDateTaken(pathsToRescan, false)
for (path in pathsToRescan) {
val file = File(path)
val lastModified = file.lastModified()
if (config.keepLastModified && lastModified != 0L) {
File(file.absolutePath).setLastModified(lastModified)
updateLastModified(file.absolutePath, lastModified)
}
}
}
activity.runOnUiThread {
dialog?.dismiss()
callback.invoke()
}
}
}
}
}
private fun setupViews(view: View) {
val handler = Handler(Looper.getMainLooper())
val resizeFactorInputLayout = view.resize_factor_input_layout
view.resize_factor_edit_text.apply {
setText(DEFAULT_RESIZE_FACTOR)
doAfterTextChanged {
resizeFactorInputLayout.error = null
handler.removeCallbacksAndMessages(null)
handler.postDelayed({
val factorText = it?.toString()
if (factorText.isNullOrEmpty() || factorText.toInt() !in 10..90) {
resizeFactorInputLayout.error = activity.getString(R.string.resize_factor_error)
} else {
resizeFactorInputLayout.error = null
}
}, RESIZE_FACTOR_ERROR_DELAY)
}
}
progressView.apply {
max = imagePaths.size
setIndicatorColor(activity.getProperPrimaryColor())
}
}
}

View file

@ -8,6 +8,7 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.Point
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.net.Uri import android.net.Uri
@ -39,6 +40,8 @@ import com.simplemobiletools.gallery.pro.activities.SettingsActivity
import com.simplemobiletools.gallery.pro.activities.SimpleActivity import com.simplemobiletools.gallery.pro.activities.SimpleActivity
import com.simplemobiletools.gallery.pro.dialogs.AllFilesPermissionDialog import com.simplemobiletools.gallery.pro.dialogs.AllFilesPermissionDialog
import com.simplemobiletools.gallery.pro.dialogs.PickDirectoryDialog import com.simplemobiletools.gallery.pro.dialogs.PickDirectoryDialog
import com.simplemobiletools.gallery.pro.dialogs.ResizeMultipleImagesDialog
import com.simplemobiletools.gallery.pro.dialogs.ResizeWithPathDialog
import com.simplemobiletools.gallery.pro.helpers.DIRECTORY import com.simplemobiletools.gallery.pro.helpers.DIRECTORY
import com.simplemobiletools.gallery.pro.helpers.RECYCLE_BIN import com.simplemobiletools.gallery.pro.helpers.RECYCLE_BIN
import com.simplemobiletools.gallery.pro.models.DateTaken import com.simplemobiletools.gallery.pro.models.DateTaken
@ -734,6 +737,124 @@ fun BaseSimpleActivity.copyFile(source: String, destination: String) {
} }
} }
fun BaseSimpleActivity.ensureWriteAccess(path: String, callback: () -> Unit) {
when {
isRestrictedSAFOnlyRoot(path) -> {
handleAndroidSAFDialog(path) {
if (!it) {
return@handleAndroidSAFDialog
}
callback.invoke()
}
}
needsStupidWritePermissions(path) -> {
handleSAFDialog(path) {
if (!it) {
return@handleSAFDialog
}
callback()
}
}
isAccessibleWithSAFSdk30(path) -> {
handleSAFDialogSdk30(path) {
if (!it) {
return@handleSAFDialogSdk30
}
callback()
}
}
else -> {
callback()
}
}
}
@TargetApi(Build.VERSION_CODES.N)
fun BaseSimpleActivity.launchResizeMultipleImagesDialog(paths: List<String>, callback: (() -> Unit)? = null) {
val imagePaths = mutableListOf<String>()
val imageSizes = mutableListOf<Point>()
for (path in paths) {
val size = path.getImageResolution(this)
if (size != null) {
imagePaths.add(path)
imageSizes.add(size)
}
}
ResizeMultipleImagesDialog(this, imagePaths, imageSizes) {
callback?.invoke()
}
}
@TargetApi(Build.VERSION_CODES.N)
fun BaseSimpleActivity.launchResizeImageDialog(path: String, callback: (() -> Unit)? = null) {
val originalSize = path.getImageResolution(this) ?: return
ResizeWithPathDialog(this, originalSize, path) { newSize, newPath ->
ensureBackgroundThread {
try {
resizeImage(newPath, newSize) { success ->
if (success) {
toast(R.string.file_saved)
val file = File(path)
val lastModified = file.lastModified()
val paths = arrayListOf(file.absolutePath)
rescanPaths(paths) {
fixDateTaken(paths, false)
if (config.keepLastModified && lastModified != 0L) {
File(file.absolutePath).setLastModified(lastModified)
updateLastModified(file.absolutePath, lastModified)
}
}
runOnUiThread {
callback?.invoke()
}
} else {
toast(R.string.image_editing_failed)
}
}
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
} catch (e: Exception) {
showErrorToast(e)
}
}
}
}
fun BaseSimpleActivity.resizeImage(path: String, size: Point, callback: (success: Boolean) -> Unit) {
var oldExif: ExifInterface? = null
if (isNougatPlus()) {
val inputStream = contentResolver.openInputStream(Uri.fromFile(File(path)))
oldExif = ExifInterface(inputStream!!)
}
val newBitmap = Glide.with(applicationContext).asBitmap().load(path).submit(size.x, size.y).get()
val newFile = File(path)
val newFileDirItem = FileDirItem(path, path.getFilenameFromPath())
getFileOutputStream(newFileDirItem, true) { out ->
if (out != null) {
out.use {
try {
newBitmap.compress(newFile.absolutePath.getCompressionFormat(), 90, out)
if (isNougatPlus()) {
val newExif = ExifInterface(newFile.absolutePath)
oldExif?.copyNonDimensionAttributesTo(newExif)
}
} catch (ignored: Exception) {
}
callback(true)
}
} else {
callback(false)
}
}
}
fun saveFile(path: String, bitmap: Bitmap, out: FileOutputStream, degrees: Int) { fun saveFile(path: String, bitmap: Bitmap, out: FileOutputStream, degrees: Int) {
val matrix = Matrix() val matrix = Matrix()
matrix.postRotate(degrees.toFloat()) matrix.postRotate(degrees.toFloat())

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/resize_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/normal_margin"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/resize_factor_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/big_margin"
android:layout_marginTop="@dimen/normal_margin"
android:text="@string/resize_factor_info"
android:textSize="@dimen/normal_text_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/resize_progress" />
<com.simplemobiletools.commons.views.MyTextInputLayout
android:id="@+id/resize_factor_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/big_margin"
android:layout_marginTop="@dimen/medium_margin"
android:hint="@string/resize_factor"
app:errorEnabled="true"
app:layout_constraintTop_toBottomOf="@id/resize_factor_info"
app:suffixText="%">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/resize_factor_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:singleLine="true"
android:textCursorDrawable="@null"
android:textSize="@dimen/bigger_text_size" />
</com.simplemobiletools.commons.views.MyTextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -83,6 +83,11 @@
android:showAsAction="never" android:showAsAction="never"
android:title="@string/set_as" android:title="@string/set_as"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/cab_resize"
android:showAsAction="never"
android:title="@string/resize"
app:showAsAction="never" />
<item <item
android:id="@+id/cab_edit" android:id="@+id/cab_edit"
android:icon="@drawable/ic_edit_vector" android:icon="@drawable/ic_edit_vector"