# HG changeset patch # User David Barts # Date 1612760914 28800 # Node ID 06825e49f7aa8ccfb7ebc78901b9d12efe6109dc # Parent f26f61a8a9ad75f9d77b295192ae058024ae549e Got it scaling and rotating, needs settings, etc. diff -r f26f61a8a9ad -r 06825e49f7aa app/src/main/java/com/bartsent/simpleresizer/EditImage.kt --- 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(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 - "${title}: ${Html.escapeHtml(value.toString())}
" - - 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("Error: no URI supplied!") + showError(getString(R.string.error_no_uri)) return } @@ -78,20 +85,186 @@ } } if (State.bitmap == null) { - binding.imageStatusReport.text = fromHtml("Error: 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(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)) + } } } diff -r f26f61a8a9ad -r 06825e49f7aa app/src/main/java/com/bartsent/simpleresizer/MainActivity.kt --- 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) { diff -r f26f61a8a9ad -r 06825e49f7aa app/src/main/res/layout/activity_edit_image.xml --- 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"> - + + + +