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()
    }
}