Mercurial > cgi-bin > hgweb.cgi > SimpleResizer
view app/src/main/java/com/bartsent/simpleresizer/EditImage.kt @ 21:7e7e71724770
No longer breaks when phone rotated.
author | David Barts <n5jrn@me.com> |
---|---|
date | Mon, 22 Feb 2021 17:49:27 -0800 |
parents | eedf995462d9 |
children | c29f941d09cd |
line wrap: on
line source
package com.bartsent.simpleresizer import android.annotation.SuppressLint import android.content.ContentValues import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Matrix import android.net.Uri import android.os.Bundle import android.os.Environment import android.provider.MediaStore import android.provider.OpenableColumns import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.EditText import android.widget.ProgressBar import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding import com.bartsent.simpleresizer.lib.getScaledInstance import java.io.File import java.io.IOException import kotlin.concurrent.thread import androidx.preference.PreferenceManager import com.bartsent.simpleresizer.lib.ThreadPools class EditImage : AppCompatActivity() { private object State { var uri: Uri? = null var bitmap: Bitmap? = null } private val STDDIMS = arrayOf<Int>(1600, 1280, 1024, 800, 640, 512, 400, 320).apply { sort() } private lateinit var binding: ActivityEditImageBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityEditImageBinding.inflate(layoutInflater) setContentView(binding.root) PreferenceManager.setDefaultValues(applicationContext, R.xml.root_preferences, false) } override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_edit, menu) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.settings_item) { startActivity( Intent(Intent.ACTION_APPLICATION_PREFERENCES, null, this, SettingsActivity::class.java)) return true } return false } // Cribbed from: https://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content private fun getFileName(uri: Uri): String? { var result: String? = null if (uri.scheme == "content") { contentResolver.query(uri, null, null, null, null).use { cursor -> if (cursor != null && cursor.moveToFirst()) result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) } } if (result == null) { val uriPath = uri.path result = uriPath?.substring(uriPath.lastIndexOf('/') + 1) } return result } private fun showFatalError(message: String): Unit { AlertDialog.Builder(this).also { it.setMessage(message) it.setNeutralButton(R.string.ok_text) { dialog, _ -> dialog.dismiss() } it.setOnDismissListener { finish() } it.create() }.show() } override fun onResume() { super.onResume() // Read the URI, die if we can't. val imageUri = intent?.data if (imageUri == null) { if (State.bitmap == null) showFatalError(getString(R.string.error_no_uri)) else setImage(State.bitmap!!) return } // User has opened a new image. if (imageUri != State.uri) { State.uri = imageUri binding.progressBar.visibility = ProgressBar.VISIBLE ThreadPools.WORKERS.execute { State.bitmap = contentResolver.openInputStream(imageUri).use { BitmapFactory.decodeStream(it) } runOnUiThread { binding.progressBar.visibility = ProgressBar.INVISIBLE if (State.bitmap == null) showFatalError(getString(R.string.error_bad_image)) else setImage(State.bitmap!!) } } return } // Rotation (of the phone). val oldBitmap = State.bitmap if (oldBitmap != null) setImage(oldBitmap) } private fun setImage(image: Bitmap): Unit { binding.imageSize.text = getString(R.string.image_size_text, image.width, image.height) binding.image.setImageBitmap(image) State.bitmap = image binding.root.invalidate() } private val CUSTOM = 999998 private val CANCEL = 999999 fun scaleClicked(view: View): Unit { val (maxSize, horizontal) = State.bitmap!!.run { if (width > height) Pair(width, true) else Pair(height, false) } PopupMenu(this, view).apply { menu.run { STDDIMS.filter { it < maxSize }.forEach { major -> val minor = major * 3 / 4 add(Menu.NONE, major, Menu.NONE, if (horizontal) "$major ✕ $minor" else "$minor ✕ $major") } add(Menu.NONE, CUSTOM, Menu.NONE, R.string.custom_text) add(Menu.NONE, CANCEL, Menu.NONE, R.string.cancel_text) } setOnMenuItemClickListener(::scaleMenuItemClicked) show() } } private fun scaleMenuItemClicked(item: MenuItem) : Boolean = when (item.itemId) { CUSTOM -> { showCustomScaleDialog(); true } CANCEL -> true in STDDIMS -> { doScale(item.itemId); true } else -> false } private fun copyColorSpace(old: Bitmap, new: Bitmap): Unit { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { val oldColorSpace = old.colorSpace if (oldColorSpace != null) new.setColorSpace(oldColorSpace) } } private fun doScale(newMax: Int): Unit { val oldBitmap = State.bitmap!! val factor = newMax.toDouble() / maxOf(oldBitmap.width, oldBitmap.height).toDouble() if (factor >= 1.0) { throw IllegalArgumentException("can only scale down") } val scaleType = PreferenceManager.getDefaultSharedPreferences(applicationContext).getString( "scale_type", "speed" ) Log.d("EditImage", "scaling, scale_type = $scaleType") binding.progressBar.visibility = ProgressBar.VISIBLE ThreadPools.WORKERS.execute { val newWidth = (oldBitmap.width.toDouble() * factor + 0.5).toInt() val newHeight = (oldBitmap.height.toDouble() * factor + 0.5).toInt() val newBitmap = if (scaleType == "quality") oldBitmap.getScaledInstance(newWidth, newHeight) else Bitmap.createScaledBitmap(oldBitmap, newWidth, newHeight, true) runOnUiThread { binding.progressBar.visibility = ProgressBar.INVISIBLE setImage(newBitmap) } } } private fun showCustomScaleDialog(): Unit { val image = State.bitmap!! val curMaxDim = maxOf(image.width, image.height) val dialogView = layoutInflater.inflate(R.layout.dialog_custom_scale, null) AlertDialog.Builder(this).also { it.setPositiveButton(R.string.ok_text) { dialog, _ -> val maxDim = dialogView.findViewById<EditText>(R.id.custom_scale)?.text.toString().toIntOrNull() dialog.dismiss() if (maxDim == null || maxDim < 8 || maxDim >= curMaxDim) { Toast.makeText(applicationContext, R.string.bad_scale, Toast.LENGTH_LONG).show() } else { doScale(maxDim) } } it.setNegativeButton(R.string.cancel_text) { dialog, _ -> dialog.dismiss() } it.setView(dialogView) it.create() }.show() } fun rotateClicked(view: View): Unit { PopupMenu(this, view).apply { setOnMenuItemClickListener { when(it.itemId) { R.id.r_90_cw -> { doRotate(90) true } R.id.r_180 -> { doRotate(180) true } R.id.r_90_ccw -> { doRotate(270) true } R.id.r_cancel -> true else -> false } } inflate(R.menu.rotate) show() } } private fun doRotate(deg: Int): Unit { val oldBitmap = State.bitmap!! if (deg % 90 != 0) { throw IllegalArgumentException("$deg not a multiple of 90") } val (w, h) = if (deg % 180 == 0) Pair(oldBitmap.width, oldBitmap.height) else Pair(oldBitmap.height, oldBitmap.width) val rotater = Matrix().apply { setRotate(deg.toFloat(), oldBitmap.width.toFloat()/2.0f, oldBitmap.height.toFloat()/2.0f) postTranslate((w - oldBitmap.width).toFloat()/2.0f, (h - oldBitmap.height).toFloat()/2.0f) } binding.progressBar.visibility = ProgressBar.VISIBLE ThreadPools.WORKERS.execute { val newBitmap = Bitmap.createBitmap(w, h, oldBitmap.config) copyColorSpace(oldBitmap, newBitmap) Canvas(newBitmap).run { drawBitmap(oldBitmap, rotater, null) } runOnUiThread { binding.progressBar.visibility = ProgressBar.INVISIBLE setImage(newBitmap) } } } fun cancelClicked(view: View): Unit { State.uri = null State.bitmap = null finish() } fun doneClicked(view: View): Unit { val contentValues = ContentValues().apply { var fileName = getFileName(State.uri!!) if (fileName == null) { val d = java.util.Date() fileName = "IMG_%tY%tm%td_%tH%tM%tS.jpg".format(d, d, d, d, d, d) } else { val dot = fileName.lastIndexOf('.') if (dot == -1) { fileName = fileName + ".jpg" } else { val fileExt = fileName.substring(dot).toLowerCase() if (fileExt !in setOf(".jpg", ".jpeg")) fileName = fileName.substring(0..dot) + ".jpg" } } put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { put(MediaStore.MediaColumns.RELATIVE_PATH, File(Environment.DIRECTORY_PICTURES, getString(R.string.app_name)).getPath()) } } binding.progressBar.visibility = ProgressBar.VISIBLE ThreadPools.WORKERS.execute { var errorMessage: String? = null try { val myUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) if (myUri == null) { throw IOException(getString(R.string.error_create_mediastore)) } val stream = contentResolver.openOutputStream(myUri) if (stream == null) { throw IOException(getString(R.string.error_get_output)) } val quality = maxOf(0, minOf(100, PreferenceManager.getDefaultSharedPreferences(applicationContext).getInt( "jpeg_quality", 85))) Log.d("EditImage", "saving, jpeg_quality = $quality") stream.use { if (!State.bitmap!!.compress(Bitmap.CompressFormat.JPEG, quality, it)) { throw IOException(getString(R.string.error_save_bitmap)) } } } catch (ioe: IOException) { errorMessage = ioe.message ?: getString(R.string.error_io) } runOnUiThread { binding.progressBar.visibility = ProgressBar.INVISIBLE if (errorMessage == null) finish() else Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_LONG).show() } } } }