Mercurial > cgi-bin > hgweb.cgi > SimpleResizer
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