comparison 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
comparison
equal deleted inserted replaced
1:f26f61a8a9ad 2:06825e49f7aa
1 package com.bartsent.simpleresizer 1 package com.bartsent.simpleresizer
2 2
3 import android.content.ContentValues
3 import android.graphics.Bitmap 4 import android.graphics.Bitmap
4 import android.graphics.BitmapFactory 5 import android.graphics.BitmapFactory
6 import android.graphics.Canvas
7 import android.graphics.Matrix
5 import android.net.Uri 8 import android.net.Uri
6 import android.os.Build 9 import android.os.Bundle
10 import android.os.Environment
11 import android.provider.MediaStore
12 import android.provider.OpenableColumns
13 import android.view.MenuItem
14 import android.view.View
15 import android.widget.EditText
16 import androidx.appcompat.app.AlertDialog
7 import androidx.appcompat.app.AppCompatActivity 17 import androidx.appcompat.app.AppCompatActivity
8 import android.os.Bundle 18 import androidx.appcompat.widget.PopupMenu
9 import android.provider.OpenableColumns
10 import android.text.Html
11 import android.text.Spanned
12 import android.view.View
13 import android.widget.TextView
14 import android.widget.Toast
15 import androidx.annotation.RequiresApi
16 import androidx.databinding.DataBindingUtil
17 import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding 19 import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding
18 20 import java.io.IOException
21 import kotlin.math.roundToInt
19 22
20 class EditImage : AppCompatActivity() { 23 class EditImage : AppCompatActivity() {
21 object State { 24 object State {
22 var uri: Uri? = null 25 var uri: Uri? = null
23 var bitmap: Bitmap? = null 26 var bitmap: Bitmap? = null
24 } 27 }
25 28
29 private val STDDIMS = arrayOf<Int>(1600, 1280, 1024, 800, 640, 512, 400, 320).apply {
30 sort()
31 }
32
26 private lateinit var binding: ActivityEditImageBinding 33 private lateinit var binding: ActivityEditImageBinding
27 34
28 override fun onCreate(savedInstanceState: Bundle?) { 35 override fun onCreate(savedInstanceState: Bundle?) {
29 super.onCreate(savedInstanceState) 36 super.onCreate(savedInstanceState)
30 binding = ActivityEditImageBinding.inflate(layoutInflater) 37 binding = ActivityEditImageBinding.inflate(layoutInflater)
31 setContentView(binding.root) 38 setContentView(binding.root)
32 } 39 }
33
34 private fun item(title: String, value: Any?): String =
35 if (value == null)
36 ""
37 else
38 "<b>${title}:</b> ${Html.escapeHtml(value.toString())}<br/>"
39
40 private fun fromHtml(input: String): Spanned =
41 if (android.os.Build.VERSION.SDK_INT >= 24) {
42 Html.fromHtml(input, Html.FROM_HTML_MODE_COMPACT)
43 } else {
44 Html.fromHtml(input)
45 }
46 40
47 // Cribbed from: https://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content 41 // Cribbed from: https://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content
48 private fun getFileName(uri: Uri): String? { 42 private fun getFileName(uri: Uri): String? {
49 var result: String? = null 43 var result: String? = null
50 if (uri.scheme == "content") { 44 if (uri.scheme == "content") {
58 result = uriPath?.substring(uriPath.lastIndexOf('/') + 1) 52 result = uriPath?.substring(uriPath.lastIndexOf('/') + 1)
59 } 53 }
60 return result 54 return result
61 } 55 }
62 56
57 private fun showError(message: String): Unit {
58 AlertDialog.Builder(this).also {
59 it.setMessage(message)
60 it.setNeutralButton(R.string.ok_text) { dialog, _ ->
61 dialog.dismiss()
62 }
63 it.setOnDismissListener {
64 finish()
65 }
66 it.create()
67 }.show()
68 }
69
63 override fun onResume() { 70 override fun onResume() {
64 super.onResume() 71 super.onResume()
65 72
66 // Read the URI, die if we can't. 73 // Read the URI, die if we can't.
67 val imageUri = intent?.data 74 val imageUri = intent?.data
68 if (imageUri == null) { 75 if (imageUri == null) {
69 binding.imageStatusReport.text = fromHtml("<b>Error:</b> no URI supplied!") 76 showError(getString(R.string.error_no_uri))
70 return 77 return
71 } 78 }
72 79
73 // Being stateful stops data loss when the phone gets rotated. 80 // Being stateful stops data loss when the phone gets rotated.
74 if (imageUri != State.uri) { 81 if (imageUri != State.uri) {
76 State.bitmap = contentResolver.openInputStream(imageUri).use { 83 State.bitmap = contentResolver.openInputStream(imageUri).use {
77 BitmapFactory.decodeStream(it) 84 BitmapFactory.decodeStream(it)
78 } 85 }
79 } 86 }
80 if (State.bitmap == null) { 87 if (State.bitmap == null) {
81 binding.imageStatusReport.text = fromHtml("<b>Error:</b> bad image!") 88 showError(getString(R.string.error_bad_image))
82 return 89 return
83 } 90 }
84 val imageBitmap = State.bitmap!! 91 setImage(State.bitmap!!)
85 92 }
86 // OK, we have the Bitmap; operate on it. 93
87 binding.imageStatusReport.text = fromHtml( 94 fun setImage(image: Bitmap): Unit {
88 item("Uri", imageUri) + 95 binding.imageSize.text = getString(R.string.image_size_text, image.width, image.height)
89 item("File-Name", getFileName((imageUri))) + 96 binding.image.setImageBitmap(image)
90 item("Byte-Count", imageBitmap.byteCount) + 97 State.bitmap = image
91 item("Density", imageBitmap.density) + 98 }
92 item("Height", imageBitmap.height) + 99
93 item("Width", imageBitmap.width) + 100 fun scaleClicked(view: View): Unit {
94 item("Has-Alpha", imageBitmap.hasAlpha()) 101 val maxSize = State.bitmap!!.run { maxOf(width, height) }
95 ) 102 PopupMenu(this, view).apply {
103 menu.run {
104 STDDIMS.filter { it < maxSize }.forEach { add(it.toString()) }
105 add(getString(R.string.custom_text))
106 add(getString(R.string.cancel_text))
107 }
108 setOnMenuItemClickListener(::scaleMenuItemClicked)
109 show()
110 }
111 }
112
113 fun scaleMenuItemClicked(item: MenuItem) : Boolean {
114 val itString = item.title.toString()
115 var itVal = itString.toIntOrNull()
116 if (itVal == null) {
117 return when (itString) {
118 getString(R.string.cancel_text) -> true
119 getString(R.string.custom_text) -> { showCustomScaleDialog(); true }
120 else -> false
121 }
122 }
123 doScale(itVal)
124 return true
125 }
126
127 private fun copyColorSpace(old: Bitmap, new: Bitmap): Unit {
128 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
129 val oldColorSpace = old.colorSpace
130 if (oldColorSpace != null)
131 new.setColorSpace(oldColorSpace)
132 }
133 }
134
135 fun doScale(newMax: Int): Unit {
136 val oldBitmap = State.bitmap!!
137 val factor = newMax.toFloat() / maxOf(oldBitmap.width, oldBitmap.height).toFloat()
138 if (factor >= 1.0) {
139 throw IllegalArgumentException("can only scale down")
140 }
141 val newBitmap = Bitmap.createBitmap((oldBitmap.width * factor).roundToInt(), (oldBitmap.height * factor).roundToInt(), oldBitmap.config)
142 copyColorSpace(oldBitmap, newBitmap)
143 val scaler = Matrix().apply { setScale(factor, factor) }
144 Canvas(newBitmap).run {
145 drawBitmap(oldBitmap, scaler, null)
146 }
147 setImage(newBitmap)
148 }
149
150 // is there any way to remember the last scale value?
151 // if so: should we?
152
153 fun showCustomScaleDialog(): Unit {
154 val image = State.bitmap!!
155 val curMaxDim = maxOf(image.width, image.height)
156 val dialogView = layoutInflater.inflate(R.layout.dialog_custom_scale, null)
157 AlertDialog.Builder(this).also {
158 it.setPositiveButton(R.string.ok_text) { dialog, _ ->
159 val maxDim = dialogView.findViewById<EditText>(R.id.custom_scale)?.text.toString().toIntOrNull()
160 dialog.dismiss()
161 if (maxDim == null || maxDim < 8 || maxDim >= curMaxDim) {
162 AlertDialog.Builder(this).also {
163 it.setMessage(R.string.bad_scale)
164 it.setNeutralButton(R.string.ok_text) { dialog, _ ->
165 dialog.dismiss()
166 }
167 it.create()
168 }.show()
169 } else {
170 doScale(maxDim)
171 }
172 }
173 it.setNegativeButton(R.string.cancel_text) { dialog, _ ->
174 dialog.dismiss()
175 }
176 it.setView(dialogView)
177 it.create()
178 }.show()
179 }
180
181 fun rotateClicked(view: View): Unit {
182 PopupMenu(this, view).apply {
183 setOnMenuItemClickListener {
184 when(it.itemId) {
185 R.id.r_90_cw -> {
186 doRotate(90)
187 true
188 }
189 R.id.r_180 -> {
190 doRotate(180)
191 true
192 }
193 R.id.r_90_ccw -> {
194 doRotate(270)
195 true
196 }
197 R.id.r_cancel -> true
198 else -> false
199 }
200 }
201 inflate(R.menu.rotate)
202 show()
203 }
204 }
205
206 fun doRotate(deg: Int): Unit {
207 val oldBitmap = State.bitmap!!
208 val newBitmap = when (deg) {
209 90, 270 -> Bitmap.createBitmap(oldBitmap.height, oldBitmap.width, oldBitmap.config)
210 180 -> Bitmap.createBitmap(oldBitmap.width, oldBitmap.height, oldBitmap.config)
211 else -> throw IllegalArgumentException("deg must be 90, 180, or 270")
212 }
213 copyColorSpace(oldBitmap, newBitmap)
214 val rotater = Matrix().apply {
215 setRotate(deg.toFloat(), oldBitmap.width.toFloat()/2.0f, oldBitmap.height.toFloat()/2.0f)
216 }
217 Canvas(newBitmap).run {
218 drawBitmap(oldBitmap, rotater, null)
219 }
220 setImage(newBitmap)
221 }
222
223 fun cancelClicked(view: View): Unit {
224 State.uri = null
225 State.bitmap = null
226 finish()
227 }
228
229 fun doneClicked(view: View): Unit {
230 val contentValues = ContentValues().apply {
231 var fileName = getFileName(State.uri!!)
232 if (fileName == null) {
233 val d = java.util.Date()
234 fileName = "IMG_%tY%tm%td_%tH%tM%tS.jpg".format(d, d, d, d, d, d)
235 } else {
236 val dot = fileName.lastIndexOf('.')
237 if (dot == -1) {
238 fileName = fileName + ".jpg"
239 } else {
240 val fileExt = fileName.substring(dot).toLowerCase()
241 if (fileExt !in setOf(".jpg", ".jpeg"))
242 fileName = fileName.substring(0..dot) + ".jpg"
243 }
244 }
245 put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
246 put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
247 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
248 put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
249 }
250 }
251 try {
252 val myUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
253 if (myUri == null) {
254 throw IOException(getString(R.string.error_create_mediastore))
255 }
256 val stream = contentResolver.openOutputStream(myUri)
257 if (stream == null) {
258 throw IOException(getString(R.string.error_get_output))
259 }
260 // fixme: use quality from settings
261 stream.use {
262 if (!State.bitmap!!.compress(Bitmap.CompressFormat.JPEG, 85, it)) {
263 throw IOException(getString(R.string.error_save_bitmap))
264 }
265 }
266 } catch (ioe: IOException) {
267 showError(ioe.message ?: getString(R.string.error_io))
268 }
96 } 269 }
97 } 270 }