1+ package fr.free.nrw.commons.edit
2+
3+ import android.animation.Animator
4+ import android.animation.Animator.AnimatorListener
5+ import android.animation.ValueAnimator
6+ import android.content.Intent
7+ import android.graphics.BitmapFactory
8+ import android.graphics.Matrix
9+ import android.graphics.drawable.BitmapDrawable
10+ import android.media.ExifInterface
11+ import android.os.Bundle
12+ import android.util.Log
13+ import android.view.animation.AccelerateDecelerateInterpolator
14+ import android.widget.ImageView
15+ import android.widget.Toast
16+ import androidx.appcompat.app.AppCompatActivity
17+ import androidx.core.graphics.rotationMatrix
18+ import androidx.core.graphics.scaleMatrix
19+ import androidx.core.net.toUri
20+ import androidx.lifecycle.ViewModelProvider
21+ import fr.free.nrw.commons.R
22+ import kotlinx.android.synthetic.main.activity_edit.btn_save
23+ import kotlinx.android.synthetic.main.activity_edit.iv
24+ import kotlinx.android.synthetic.main.activity_edit.rotate_btn
25+ import timber.log.Timber
26+ import java.io.File
27+
28+ /* *
29+ * An activity class for editing and rotating images using LLJTran with EXIF attribute preservation.
30+ *
31+ * This activity allows loads an image, allows users to rotate it by 90-degree increments, and
32+ * save the edited image while preserving its EXIF attributes. The class includes methods
33+ * for initializing the UI, animating image rotations, copying EXIF data, and handling
34+ * the image-saving process.
35+ */
36+ class EditActivity : AppCompatActivity () {
37+ private var imageUri = " "
38+ private lateinit var vm: EditViewModel
39+ private val sourceExifAttributeList = mutableListOf<Pair <String , String ?>>()
40+
41+ override fun onCreate (savedInstanceState : Bundle ? ) {
42+ super .onCreate(savedInstanceState)
43+ setContentView(R .layout.activity_edit)
44+ supportActionBar?.title = " "
45+ val intent = intent
46+ imageUri = intent.getStringExtra(" image" ) ? : " "
47+ vm = ViewModelProvider (this ).get(EditViewModel ::class .java)
48+ val sourceExif = imageUri.toUri().path?.let { ExifInterface (it) }
49+ val exifTags = arrayOf(
50+ ExifInterface .TAG_APERTURE ,
51+ ExifInterface .TAG_DATETIME ,
52+ ExifInterface .TAG_EXPOSURE_TIME ,
53+ ExifInterface .TAG_FLASH ,
54+ ExifInterface .TAG_FOCAL_LENGTH ,
55+ ExifInterface .TAG_GPS_ALTITUDE ,
56+ ExifInterface .TAG_GPS_ALTITUDE_REF ,
57+ ExifInterface .TAG_GPS_DATESTAMP ,
58+ ExifInterface .TAG_GPS_LATITUDE ,
59+ ExifInterface .TAG_GPS_LATITUDE_REF ,
60+ ExifInterface .TAG_GPS_LONGITUDE ,
61+ ExifInterface .TAG_GPS_LONGITUDE_REF ,
62+ ExifInterface .TAG_GPS_PROCESSING_METHOD ,
63+ ExifInterface .TAG_GPS_TIMESTAMP ,
64+ ExifInterface .TAG_IMAGE_LENGTH ,
65+ ExifInterface .TAG_IMAGE_WIDTH ,
66+ ExifInterface .TAG_ISO ,
67+ ExifInterface .TAG_MAKE ,
68+ ExifInterface .TAG_MODEL ,
69+ ExifInterface .TAG_ORIENTATION ,
70+ ExifInterface .TAG_WHITE_BALANCE ,
71+ ExifInterface .WHITEBALANCE_AUTO ,
72+ ExifInterface .WHITEBALANCE_MANUAL
73+ )
74+ for (tag in exifTags) {
75+ val attribute = sourceExif?.getAttribute(tag.toString())
76+ sourceExifAttributeList.add(Pair (tag.toString(), attribute))
77+ }
78+
79+ init ()
80+ }
81+
82+ /* *
83+ * Initializes the ImageView and associated UI elements.
84+ *
85+ * This function sets up the ImageView for displaying an image, adjusts its view bounds,
86+ * and scales the initial image to fit within the ImageView. It also sets click listeners
87+ * for the "Rotate" and "Save" buttons.
88+ */
89+ private fun init () {
90+ iv.adjustViewBounds = true
91+ iv.scaleType = ImageView .ScaleType .MATRIX
92+ iv.post(Runnable {
93+ val bitmap = BitmapFactory .decodeFile(imageUri)
94+ iv.setImageBitmap(bitmap)
95+ if (bitmap.width > 0 ) {
96+ val scale =
97+ iv.measuredWidth.toFloat() / (iv.drawable as BitmapDrawable ).bitmap.width.toFloat()
98+ iv.layoutParams.height =
99+ (scale * (iv.drawable as BitmapDrawable ).bitmap.height).toInt()
100+ iv.imageMatrix = scaleMatrix(scale, scale)
101+ }
102+ })
103+ rotate_btn.setOnClickListener {
104+ animateImageHeight()
105+ }
106+ btn_save.setOnClickListener {
107+ getRotatedImage()
108+ }
109+ }
110+
111+ var imageRotation = 0
112+
113+ /* *
114+ * Animates the height, rotation, and scale of an ImageView to provide a smooth
115+ * transition effect when rotating an image by 90 degrees.
116+ *
117+ * This function calculates the new height, rotation, and scale for the ImageView
118+ * based on the current image rotation angle and animates the changes using a
119+ * ValueAnimator. It also disables a rotate button during the animation to prevent
120+ * further rotation actions.
121+ */
122+ private fun animateImageHeight () {
123+ val drawableWidth: Float = iv.getDrawable().getIntrinsicWidth().toFloat()
124+ val drawableHeight: Float = iv.getDrawable().getIntrinsicHeight().toFloat()
125+ val viewWidth: Float = iv.getMeasuredWidth().toFloat()
126+ val viewHeight: Float = iv.getMeasuredHeight().toFloat()
127+ val rotation = imageRotation % 360
128+ val newRotation = rotation + 90
129+
130+ val newViewHeight: Int
131+ val imageScale: Float
132+ val newImageScale: Float
133+
134+ Timber .d(" Rotation $rotation " )
135+ Timber .d(" new Rotation $newRotation " )
136+
137+
138+ if (rotation == 0 || rotation == 180 ) {
139+ imageScale = viewWidth / drawableWidth
140+ newImageScale = viewWidth / drawableHeight
141+ newViewHeight = (drawableWidth * newImageScale).toInt()
142+ } else if (rotation == 90 || rotation == 270 ) {
143+ imageScale = viewWidth / drawableHeight
144+ newImageScale = viewWidth / drawableWidth
145+ newViewHeight = (drawableHeight * newImageScale).toInt()
146+ } else {
147+ throw UnsupportedOperationException (" rotation can 0, 90, 180 or 270. \$ {rotation} is unsupported" )
148+ }
149+
150+ val animator = ValueAnimator .ofFloat(0f , 1f ).setDuration(1000L )
151+
152+ animator.interpolator = AccelerateDecelerateInterpolator ()
153+
154+ animator.addListener(object : AnimatorListener {
155+ override fun onAnimationStart (animation : Animator ) {
156+ rotate_btn.setEnabled(false )
157+ }
158+
159+ override fun onAnimationEnd (animation : Animator ) {
160+ imageRotation = newRotation % 360
161+ rotate_btn.setEnabled(true )
162+ }
163+
164+ override fun onAnimationCancel (animation : Animator ) {
165+ }
166+
167+ override fun onAnimationRepeat (animation : Animator ) {
168+ }
169+
170+ })
171+
172+ animator.addUpdateListener { animation ->
173+ val animVal = animation.animatedValue as Float
174+ val complementaryAnimVal = 1 - animVal
175+ val animatedHeight =
176+ (complementaryAnimVal * viewHeight + animVal * newViewHeight).toInt()
177+ val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
178+ val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
179+ iv.getLayoutParams().height = animatedHeight
180+ val matrix: Matrix = rotationMatrix(
181+ animatedRotation,
182+ drawableWidth / 2 ,
183+ drawableHeight / 2
184+ )
185+ matrix.postScale(
186+ animatedScale,
187+ animatedScale,
188+ drawableWidth / 2 ,
189+ drawableHeight / 2
190+ )
191+ matrix.postTranslate(
192+ - (drawableWidth - iv.getMeasuredWidth()) / 2 ,
193+ - (drawableHeight - iv.getMeasuredHeight()) / 2
194+ )
195+ iv.setImageMatrix(matrix)
196+ iv.requestLayout()
197+ }
198+
199+ animator.start()
200+ }
201+
202+ /* *
203+ * Rotates and edits the current image, copies EXIF data, and returns the edited image path.
204+ *
205+ * This function retrieves the path of the current image specified by `imageUri`,
206+ * rotates it based on the `imageRotation` angle using the `rotateImage` method
207+ * from the `vm`, and updates the EXIF attributes of the
208+ * rotated image based on the `sourceExifAttributeList`. It then copies the EXIF data
209+ * using the `copyExifData` method, creates an Intent to return the edited image's file path
210+ * as a result, and finishes the current activity.
211+ */
212+ fun getRotatedImage () {
213+
214+ val filePath = imageUri.toUri().path
215+ val file = filePath?.let { File (it) }
216+
217+
218+ val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
219+ if (rotatedImage == null ) {
220+ Toast .makeText(this , " Failed to rotate to image" , Toast .LENGTH_LONG ).show()
221+ }
222+ val editedImageExif = rotatedImage?.path?.let { ExifInterface (it) }
223+ copyExifData(editedImageExif)
224+ val resultIntent = Intent ()
225+ resultIntent.putExtra(" editedImageFilePath" , rotatedImage?.toUri()?.path ? : " Error" );
226+ setResult(RESULT_OK , resultIntent);
227+ finish();
228+ }
229+
230+ /* *
231+ * Copies EXIF data from sourceExifAttributeList to the provided ExifInterface object.
232+ *
233+ * This function iterates over the `sourceExifAttributeList` and sets the EXIF attributes
234+ * on the provided `editedImageExif` object.
235+ *
236+ * @param editedImageExif The ExifInterface object for the edited image.
237+ */
238+ private fun copyExifData (editedImageExif : ExifInterface ? ) {
239+
240+ for (attr in sourceExifAttributeList) {
241+ Log .d(" Tag is ${attr.first} " , " Value is ${attr.second} " )
242+ editedImageExif!! .setAttribute(attr.first, attr.second)
243+ Log .d(" Tag is ${attr.first} " , " Value is ${attr.second} " )
244+ }
245+
246+ editedImageExif?.saveAttributes()
247+ }
248+
249+ }
0 commit comments