changeset 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
files app/src/main/java/com/bartsent/simpleresizer/EditImage.kt app/src/main/java/com/bartsent/simpleresizer/MainActivity.kt app/src/main/res/layout/activity_edit_image.xml app/src/main/res/layout/dialog_custom_scale.xml app/src/main/res/menu/rotate.xml app/src/main/res/values/strings.xml
diffstat 6 files changed, 311 insertions(+), 44 deletions(-) [+]
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))
+        }
     }
 }
--- a/app/src/main/java/com/bartsent/simpleresizer/MainActivity.kt	Mon Feb 01 08:30:12 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/MainActivity.kt	Sun Feb 07 21:08:34 2021 -0800
@@ -2,11 +2,10 @@
 
 import android.app.Activity
 import android.content.Intent
-import android.graphics.BitmapFactory
-import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
 import androidx.appcompat.app.AppCompatActivity
-import android.os.Bundle
-import android.widget.Toast
 import com.bartsent.simpleresizer.databinding.ActivityMainBinding
 
 class MainActivity : AppCompatActivity() {
@@ -30,6 +29,7 @@
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
         super.onActivityResult(requestCode, resultCode, data)
+        Log.d("MainActivity", "onActivityResult called")
         if (requestCode == GET_IMAGE) {
             val imageUri = data?.data
             if (resultCode == Activity.RESULT_OK && imageUri != null) {
--- a/app/src/main/res/layout/activity_edit_image.xml	Mon Feb 01 08:30:12 2021 -0800
+++ b/app/src/main/res/layout/activity_edit_image.xml	Sun Feb 07 21:08:34 2021 -0800
@@ -6,13 +6,70 @@
     android:layout_height="match_parent"
     tools:context=".EditImage">
 
-    <TextView
-        android:id="@+id/image_status_report"
+    <ImageView
+        android:id="@+id/image"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="TextView"
-        app:layout_constraintBottom_toBottomOf="parent"
+        android:layout_marginBottom="5dp"
+        android:contentDescription="Image being processed."
+        android:scaleType="centerInside"
+        app:layout_constraintBottom_toTopOf="@id/scale_button"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/image_size"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="5dp"
+        android:text="TextView"
+        app:layout_constraintStart_toStartOf="@id/image"
+        app:layout_constraintEnd_toEndOf="@id/image"
+        app:layout_constraintTop_toBottomOf="@id/image" />
+
+    <Button
+        android:id="@+id/scale_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:onClick="scaleClicked"
+        android:text="@string/scale_text"
+        app:layout_constraintBottom_toTopOf="@id/cancel_button"
+        app:layout_constraintEnd_toStartOf="@id/rotate_button"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/image_size" />
+
+    <Button
+        android:id="@+id/rotate_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:onClick="rotateClicked"
+        android:text="@string/rotate_text"
+        app:layout_constraintBottom_toTopOf="@id/done_button"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/scale_button"
+        app:layout_constraintTop_toBottomOf="@id/image_size" />
+
+    <Button
+        android:id="@+id/cancel_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:onClick="cancelClicked"
+        android:text="@string/cancel_text"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/done_button"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/scale_button" />
+
+    <Button
+        android:id="@+id/done_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:onClick="doneClicked"
+        android:text="@string/done_text"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/cancel_button"
+        app:layout_constraintTop_toBottomOf="@id/rotate_button" />
+
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/src/main/res/layout/dialog_custom_scale.xml	Sun Feb 07 21:08:34 2021 -0800
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <EditText
+        android:id="@+id/custom_scale"
+        android:inputType="number"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:hint="@string/custom_scale_hint" />
+</LinearLayout>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/src/main/res/menu/rotate.xml	Sun Feb 07 21:08:34 2021 -0800
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/r_90_cw" android:title="@string/r_90_cw" />
+    <item android:id="@+id/r_180" android:title="@string/r_180" />
+    <item android:id="@+id/r_90_ccw" android:title="@string/r_90_ccw" />
+    <item android:id="@+id/r_cancel" android:title="@string/cancel_text" />
+</menu>
--- a/app/src/main/res/values/strings.xml	Mon Feb 01 08:30:12 2021 -0800
+++ b/app/src/main/res/values/strings.xml	Sun Feb 07 21:08:34 2021 -0800
@@ -1,5 +1,22 @@
 <resources>
     <string name="app_name">Simple Resizer</string>
+    <string name="bad_scale">Invalid maximum dimension.</string>
+    <string name="cancel_text">Cancel</string>
+    <string name="custom_scale_hint">max. dimension</string>
+    <string name="custom_text">Custom</string>
+    <string name="done_text">Done</string>
+    <string name="error_bad_image">Bad image!</string>
+    <string name="error_create_mediastore">Failed to create new MediaStore record.</string>
+    <string name="error_get_output">Failed to get output stream.</string>
+    <string name="error_io">I/O error.</string>
+    <string name="error_no_uri">No URI supplied!</string>
+    <string name="error_save_bitmap">Failed to save bitmap.</string>
+    <string name="image_size_text">Width: %d, Height: %d.</string>
+    <string name="ok_text">OK</string>
+    <string name="r_180">180˚</string>
+    <string name="r_90_ccw">90˚ CCW</string>
+    <string name="r_90_cw">90˚ CW</string>
+    <string name="rotate_text">Rotate</string>
+    <string name="scale_text">Scale</string>
     <string name="selecting_message">Selecting image…</string>
-
 </resources>
\ No newline at end of file