Mercurial > cgi-bin > hgweb.cgi > SimpleResizer
view app/src/main/java/com/bartsent/simpleresizer/EditImage.kt @ 5:247e03baf77c
Fix rotation.
author | David Barts <n5jrn@me.com> |
---|---|
date | Wed, 10 Feb 2021 16:31:01 -0800 |
parents | b6a217c850fb |
children | e8059b166de1 |
line wrap: on
line source
package com.bartsent.simpleresizer import android.content.ContentValues 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.view.MenuItem import android.view.View import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding import java.io.File import java.io.IOException import kotlin.math.roundToInt 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) } // 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 showError(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) { showError(getString(R.string.error_no_uri)) return } // Being stateful stops data loss when the phone gets rotated. if (imageUri != State.uri) { State.uri = imageUri State.bitmap = contentResolver.openInputStream(imageUri).use { BitmapFactory.decodeStream(it) } } if (State.bitmap == null) { showError(getString(R.string.error_bad_image)) return } setImage(State.bitmap!!) } 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 } fun scaleClicked(view: View): Unit { val maxSize = State.bitmap!!.run { maxOf(width, height) } PopupMenu(this, view).apply { menu.run { STDDIMS.filter { it < maxSize }.forEach { add(it.toString()) } add(getString(R.string.custom_text)) add(getString(R.string.cancel_text)) } setOnMenuItemClickListener(::scaleMenuItemClicked) show() } } fun scaleMenuItemClicked(item: MenuItem) : Boolean { val itString = item.title.toString() var itVal = itString.toIntOrNull() if (itVal == null) { return when (itString) { getString(R.string.cancel_text) -> true getString(R.string.custom_text) -> { showCustomScaleDialog(); true } else -> false } } doScale(itVal) return true } 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) } } fun doScale(newMax: Int): Unit { val oldBitmap = State.bitmap!! val factor = newMax.toFloat() / maxOf(oldBitmap.width, oldBitmap.height).toFloat() if (factor >= 1.0) { throw IllegalArgumentException("can only scale down") } val newBitmap = Bitmap.createBitmap((oldBitmap.width * factor).roundToInt(), (oldBitmap.height * factor).roundToInt(), oldBitmap.config) copyColorSpace(oldBitmap, newBitmap) val scaler = Matrix().apply { setScale(factor, factor) } Canvas(newBitmap).run { drawBitmap(oldBitmap, scaler, null) } setImage(newBitmap) } // is there any way to remember the last scale value? // if so: should we? 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) { AlertDialog.Builder(this).also { it.setMessage(R.string.bad_scale) it.setNeutralButton(R.string.ok_text) { dialog, _ -> dialog.dismiss() } it.create() }.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() } } 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 newBitmap = Bitmap.createBitmap(w, h, oldBitmap.config) copyColorSpace(oldBitmap, newBitmap) 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) } Canvas(newBitmap).run { drawBitmap(oldBitmap, rotater, null) } 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()) } } 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)) } // fixme: use quality from settings stream.use { if (!State.bitmap!!.compress(Bitmap.CompressFormat.JPEG, 85, it)) { throw IOException(getString(R.string.error_save_bitmap)) } } } catch (ioe: IOException) { showError(ioe.message ?: getString(R.string.error_io)) } finish() } }