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