diff app/src/main/java/com/bartsent/simpleresizer/EditImage.kt @ 2:06825e49f7aa

Got it scaling and rotating, needs settings, etc.
author David Barts <n5jrn@me.com>
date Sun, 07 Feb 2021 21:08:34 -0800
parents f26f61a8a9ad
children 21d2df45d350
line wrap: on
line diff
--- a/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Mon Feb 01 08:30:12 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Sun Feb 07 21:08:34 2021 -0800
@@ -1,21 +1,24 @@
 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.Build
-import androidx.appcompat.app.AppCompatActivity
 import android.os.Bundle
+import android.os.Environment
+import android.provider.MediaStore
 import android.provider.OpenableColumns
-import android.text.Html
-import android.text.Spanned
+import android.view.MenuItem
 import android.view.View
-import android.widget.TextView
-import android.widget.Toast
-import androidx.annotation.RequiresApi
-import androidx.databinding.DataBindingUtil
+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.IOException
+import kotlin.math.roundToInt
 
 class EditImage : AppCompatActivity() {
     object State {
@@ -23,6 +26,10 @@
         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?) {
@@ -31,19 +38,6 @@
         setContentView(binding.root)
     }
 
-    private fun item(title: String, value: Any?): String =
-        if (value == null)
-            ""
-        else
-            "<b>${title}:</b> ${Html.escapeHtml(value.toString())}<br/>"
-
-    private fun fromHtml(input: String): Spanned =
-        if (android.os.Build.VERSION.SDK_INT >= 24) {
-            Html.fromHtml(input, Html.FROM_HTML_MODE_COMPACT)
-        } else {
-            Html.fromHtml(input)
-        }
-
     // 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
@@ -60,13 +54,26 @@
         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) {
-            binding.imageStatusReport.text = fromHtml("<b>Error:</b> no URI supplied!")
+            showError(getString(R.string.error_no_uri))
             return
         }
 
@@ -78,20 +85,186 @@
             }
         }
         if (State.bitmap == null) {
-            binding.imageStatusReport.text = fromHtml("<b>Error:</b> bad image!")
+            showError(getString(R.string.error_bad_image))
             return
         }
-        val imageBitmap = State.bitmap!!
+        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?
 
-        // OK, we have the Bitmap; operate on it.
-        binding.imageStatusReport.text = fromHtml(
-                item("Uri", imageUri) +
-                        item("File-Name", getFileName((imageUri))) +
-                        item("Byte-Count", imageBitmap.byteCount) +
-                        item("Density", imageBitmap.density) +
-                        item("Height", imageBitmap.height) +
-                        item("Width", imageBitmap.width) +
-                        item("Has-Alpha", imageBitmap.hasAlpha())
-        )
+    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!!
+        val newBitmap = when (deg) {
+            90, 270 -> Bitmap.createBitmap(oldBitmap.height, oldBitmap.width, oldBitmap.config)
+            180 -> Bitmap.createBitmap(oldBitmap.width, oldBitmap.height, oldBitmap.config)
+            else -> throw IllegalArgumentException("deg must be 90, 180, or 270")
+        }
+        copyColorSpace(oldBitmap, newBitmap)
+        val rotater = Matrix().apply {
+            setRotate(deg.toFloat(), oldBitmap.width.toFloat()/2.0f, 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, Environment.DIRECTORY_PICTURES)
+            }
+        }
+        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))
+        }
     }
 }