From b5e15463c403aa2e3ac96b9d09961ffddca5ad50 Mon Sep 17 00:00:00 2001 From: Owm Dubey Date: Sun, 22 Feb 2026 19:37:43 +0530 Subject: [PATCH] Fix:rebase Signed-off-by: Owm Dubey # Conflicts: # app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt # Conflicts: # app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt # app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt --- app/build.gradle.kts | 4 + .../fr/free/nrw/commons/CommonsApplication.kt | 13 ++- .../fr/free/nrw/commons/edit/EditActivity.kt | 110 ++++++++++++------ .../nrw/commons/media/MediaDetailFragment.kt | 36 ++++-- .../nrw/commons/upload/ThumbnailsAdapter.kt | 12 +- .../mediaDetails/UploadMediaDetailFragment.kt | 16 ++- .../fr/free/nrw/commons/utils/ImageUtils.kt | 3 + .../main/res/layout/fragment_media_detail.xml | 8 ++ .../main/res/layout/item_upload_thumbnail.xml | 9 ++ gradle/libs.versions.toml | 3 + 10 files changed, 166 insertions(+), 48 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 175a1d101e2..f27604d9b7d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -355,6 +355,10 @@ dependencies { kaptTest(libs.androidx.databinding.compiler) kaptAndroidTest(libs.androidx.databinding.compiler) + // Coil + implementation(libs.coil) + implementation(libs.coil.svg) + implementation(libs.coordinates2country.android) { exclude(group = "com.google.android", module = "android") } diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt index 89fdaa055c3..dd342f6437b 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt @@ -11,6 +11,9 @@ import android.os.Build import android.os.Process import android.util.Log import androidx.multidex.MultiDexApplication +import coil.Coil +import coil.ImageLoader +import coil.decode.SvgDecoder import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.imagepipeline.core.ImagePipelineConfig import fr.free.nrw.commons.auth.LoginActivity @@ -66,7 +69,7 @@ import javax.inject.Named resCommentPrompt = R.string.crash_dialog_comment_prompt ) -class CommonsApplication : MultiDexApplication() { +class CommonsApplication : MultiDexApplication(), coil.ImageLoaderFactory { @Inject lateinit var sessionManager: SessionManager @@ -139,6 +142,14 @@ class CommonsApplication : MultiDexApplication() { System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0") } + override fun newImageLoader(): ImageLoader { + return ImageLoader.Builder(this) + .components { + add(SvgDecoder.Factory()) + } + .build() + } + /** * Plants debug and file logging tree. Timber lets you plant your own logging trees. */ diff --git a/app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt b/app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt index 8587e940176..49e04edd5ab 100644 --- a/app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt @@ -22,6 +22,7 @@ import androidx.core.net.toUri import androidx.core.view.WindowInsetsCompat import androidx.exifinterface.media.ExifInterface import androidx.lifecycle.ViewModelProvider +import coil.load import fr.free.nrw.commons.databinding.ActivityEditBinding import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomInsets import fr.free.nrw.commons.utils.applyEdgeToEdgeTopPaddingInsets @@ -104,9 +105,12 @@ class EditActivity : AppCompatActivity() { ExifInterface.TAG_GPS_LONGITUDE_REF, ExifInterface.TAG_GPS_PROCESSING_METHOD, ExifInterface.TAG_GPS_TIMESTAMP, + ExifInterface.TAG_IMAGE_LENGTH, + ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, ExifInterface.TAG_MAKE, ExifInterface.TAG_MODEL, + ExifInterface.TAG_ORIENTATION, ExifInterface.TAG_WHITE_BALANCE, ExifInterface.WHITE_BALANCE_AUTO, ExifInterface.WHITE_BALANCE_MANUAL, @@ -116,6 +120,11 @@ class EditActivity : AppCompatActivity() { sourceExifAttributeList.add(Pair(tag.toString(), attribute)) } + if (imageUri.substringBefore("?").endsWith(".svg", ignoreCase = true)) { + Toast.makeText(this, "SVG files cannot be edited", Toast.LENGTH_SHORT).show() + finish() + return + } init() } @@ -124,16 +133,28 @@ class EditActivity : AppCompatActivity() { * * This function sets up the ImageView for displaying an image, adjusts its view bounds, * and scales the initial image to fit within the ImageView. It also sets click listeners - * for the "Rotate", "Crop" and "Save" buttons. + * for the "Rotate" and "Save" buttons. */ private fun init() { binding.iv.adjustViewBounds = true binding.iv.scaleType = ImageView.ScaleType.MATRIX - binding.iv.post { - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - BitmapFactory.decodeFile(imageUri, options) - + if (imageUri.substringBefore("?").endsWith(".svg", ignoreCase = true)) { + binding.rotateBtn.isEnabled = false + binding.iv.load(imageUri) { + listener( + onSuccess = { _, result -> + val drawable = result.drawable + val scale = binding.iv.measuredWidth.toFloat() / drawable.intrinsicWidth.toFloat() + binding.iv.layoutParams.height = (scale * drawable.intrinsicHeight).toInt() + binding.iv.imageMatrix = scaleMatrix(scale, scale) + } + ) + } + } else { + binding.iv.post { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(imageUri, options) val bitmapWidth = options.outWidth val bitmapHeight = options.outHeight @@ -193,7 +214,7 @@ class EditActivity : AppCompatActivity() { binding.iv.imageMatrix = matrix } - } + }} binding.rotateBtn.setOnClickListener { // Allow rotation while in crop mode - overlay will update after animation animateImageHeight() @@ -416,6 +437,16 @@ class EditActivity : AppCompatActivity() { var imageRotation = 0 + /** + * Data class representing crop coordinates. + */ + private data class CropCoordinates( + val left: Int, + val top: Int, + val width: Int, + val height: Int + ) + /** * Animates the height, rotation, and scale of an ImageView to provide a smooth * transition effect when rotating an image by 90 degrees. @@ -452,28 +483,24 @@ class EditActivity : AppCompatActivity() { when (rotation) { 0, 180 -> { - imageScale = min(viewWidth / drawableWidth, maxAvailableHeight / drawableHeight) - val fitW = viewWidth / drawableHeight - val fitH = maxAvailableHeight / drawableWidth - newImageScale = min(fitW, fitH) - newViewHeight = min((drawableWidth * newImageScale).toInt(), maxAvailableHeight.toInt()) + imageScale = viewWidth / drawableWidth + newImageScale = viewWidth / drawableHeight + newViewHeight = (drawableWidth * newImageScale).toInt() } 90, 270 -> { - imageScale = min(viewWidth / drawableHeight, maxAvailableHeight / drawableWidth) - val fitW = viewWidth / drawableWidth - val fitH = maxAvailableHeight / drawableHeight - newImageScale = min(fitW, fitH) - newViewHeight = min((drawableHeight * newImageScale).toInt(), maxAvailableHeight.toInt()) + imageScale = viewWidth / drawableHeight + newImageScale = viewWidth / drawableWidth + newViewHeight = (drawableHeight * newImageScale).toInt() } else -> { throw UnsupportedOperationException( - "rotation can 0, 90, 180 or 270. \${rotation} is unsupported" + "rotation can 0, 90, 180 or 270. $rotation is unsupported" ) } } - val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(500L) + val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000L) animator.interpolator = AccelerateDecelerateInterpolator() @@ -486,12 +513,6 @@ class EditActivity : AppCompatActivity() { override fun onAnimationEnd(animation: Animator) { imageRotation = newRotation % 360 binding.rotateBtn.setEnabled(true) - - // If crop mode is active, update the overlay bounds for new rotation - // Use post{} to wait for the layout pass triggered by requestLayout() - if (isCropMode) { - binding.iv.post { updateCropOverlayBounds() } - } } override fun onAnimationCancel(animation: Animator) { @@ -533,6 +554,35 @@ class EditActivity : AppCompatActivity() { animator.start() } + /** + * Rotates and edits the current image, copies EXIF data, and returns the edited image path. + * + * This function retrieves the path of the current image specified by `imageUri`, + * rotates it based on the `imageRotation` angle using the `rotateImage` method + * from the `vm`, and updates the EXIF attributes of the + * rotated image based on the `sourceExifAttributeList`. It then copies the EXIF data + * using the `copyExifData` method, creates an Intent to return the edited image's file path + * as a result, and finishes the current activity. + */ + fun getRotatedImage() { + val filePath = imageUri.toUri().path + val file = filePath?.let { File(it) } + + val rotatedImage = file?.let { vm.rotateImage(imageRotation, it, applicationContext.cacheDir) } + if (rotatedImage == null) { + Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show() + } + val editedImageExif: ExifInterface? + if (rotatedImage?.path != null) { + editedImageExif = ExifInterface(rotatedImage.path) + copyExifData(editedImageExif) + } + val resultIntent = Intent() + resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error") + setResult(RESULT_OK, resultIntent) + finish() + } + /** * Copies EXIF data from sourceExifAttributeList to the provided ExifInterface object. * @@ -585,13 +635,3 @@ class EditActivity : AppCompatActivity() { return scaleFactor } } - -/** - * Data class to hold crop coordinates. - */ -private data class CropCoordinates( - val left: Int, - val top: Int, - val width: Int, - val height: Int -) diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt index ab089c48b5c..52e87aa39af 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt @@ -24,6 +24,7 @@ import android.widget.LinearLayout import android.widget.Spinner import android.widget.TextView import android.widget.Toast +import coil.load import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement @@ -733,19 +734,34 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C val imageBackgroundColor: Int = imageBackgroundColor if (imageBackgroundColor != DEFAULT_IMAGE_BACKGROUND_COLOR) { binding.mediaDetailImageView.setBackgroundColor(imageBackgroundColor) + binding.mediaDetailImageViewSvg.setBackgroundColor(imageBackgroundColor) } - binding.mediaDetailImageView.hierarchy.setPlaceholderImage(R.drawable.image_placeholder) - binding.mediaDetailImageView.hierarchy.setFailureImage(R.drawable.image_placeholder) + val mediaUrl = media?.imageUrl + if (mediaUrl != null && mediaUrl.substringBefore("?").endsWith(".svg", ignoreCase = true)) { + binding.mediaDetailImageView.visibility = View.GONE + binding.mediaDetailImageViewSvg.visibility = View.VISIBLE - val controller: DraweeController = Fresco.newDraweeControllerBuilder() - .setLowResImageRequest(ImageRequest.fromUri(if (media != null) media!!.thumbUrl else null)) - .setRetainImageOnFailure(true) - .setImageRequest(ImageRequest.fromUri(if (media != null) media!!.imageUrl else null)) - .setControllerListener(aspectRatioListener) - .setOldController(binding.mediaDetailImageView.controller) - .build() - binding.mediaDetailImageView.controller = controller + binding.mediaDetailImageViewSvg.load(mediaUrl) { + placeholder(R.drawable.image_placeholder) + error(R.drawable.image_placeholder) + } + } else { + binding.mediaDetailImageViewSvg.visibility = View.GONE + binding.mediaDetailImageView.visibility = View.VISIBLE + + binding.mediaDetailImageView.hierarchy.setPlaceholderImage(R.drawable.image_placeholder) + binding.mediaDetailImageView.hierarchy.setFailureImage(R.drawable.image_placeholder) + + val controller: DraweeController = Fresco.newDraweeControllerBuilder() + .setLowResImageRequest(ImageRequest.fromUri(if (media != null) media!!.thumbUrl else null)) + .setRetainImageOnFailure(true) + .setImageRequest(ImageRequest.fromUri(if (media != null) media!!.imageUrl else null)) + .setControllerListener(aspectRatioListener) + .setOldController(binding.mediaDetailImageView.controller) + .build() + binding.mediaDetailImageView.controller = controller + } } private fun updateToDoWarning() { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt index d467f9bf63c..5dc7a3aa63e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt @@ -6,8 +6,10 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView import android.widget.RelativeLayout +import android.view.View import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import coil.load import com.facebook.drawee.view.SimpleDraweeView import fr.free.nrw.commons.R import fr.free.nrw.commons.databinding.ItemUploadThumbnailBinding @@ -50,7 +52,15 @@ internal class ThumbnailsAdapter(private val callback: Callback) : fun bind(position: Int) { val uploadableFile = uploadableFiles[position] val uri = uploadableFile.getMediaUri() - background.setImageURI(Uri.fromFile(File(uri.toString()))) + if (uri.toString().substringBefore("?").endsWith(".svg", ignoreCase = true)) { + background.visibility = View.GONE + binding.ivThumbnailSvg.visibility = View.VISIBLE + binding.ivThumbnailSvg.load(uri) + } else { + background.visibility = View.VISIBLE + binding.ivThumbnailSvg.visibility = View.GONE + background.setImageURI(Uri.fromFile(File(uri.toString()))) + } if (position == callback.getCurrentSelectedFilePosition()) { val border = GradientDrawable() border.shape = GradientDrawable.RECTANGLE diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt index fa83a7ccc23..7a96d962eb7 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt @@ -20,7 +20,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.exifinterface.media.ExifInterface +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import coil.load import com.github.chrisbanes.photoview.PhotoView import fr.free.nrw.commons.CameraPosition import fr.free.nrw.commons.R @@ -52,6 +54,9 @@ import fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished import fr.free.nrw.commons.utils.ViewUtil.showLongToast import fr.free.nrw.commons.utils.handleKeyboardInsets +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File import java.util.ArrayList @@ -289,8 +294,11 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra View.VISIBLE } - // lljtran only supports lossless JPEG rotation, so we disable editing for other formatts val filePath = uploadableFile?.getFilePath()?.toString() ?: "" + val isSvgFile = filePath.endsWith(".svg", ignoreCase = true) || + uploadItem?.mediaUri?.toString()?.substringBefore("?")?.endsWith(".svg", ignoreCase = true) == true + llEditImage.visibility = if (isSvgFile) View.GONE else View.VISIBLE + // lljtran only supports lossless JPEG rotation, so we disable editing for other formatts val isJpeg = filePath.endsWith(".jpeg", ignoreCase = true) || filePath.endsWith(".jpg", ignoreCase = true) llEditImage.visibility = View.VISIBLE @@ -394,6 +402,12 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra if (_binding == null) { return } + val mediaUri = uploadItem.mediaUri + if (mediaUri != null && mediaUri.toString().substringBefore("?").endsWith(".svg", ignoreCase = true)) { + binding.backgroundImage.load(mediaUri) + } else { + binding.backgroundImage.setImageURI(mediaUri) + } // If the user has already edited the image, show the edited version // instead of the original. This prevents the async preProcessImage // callback from overwriting the edited preview. diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt index fa538bb2154..f01f77ea7c0 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt @@ -102,6 +102,9 @@ object ImageUtils { */ @JvmStatic fun checkIfImageIsTooDark(imagePath: String): Int { + if (imagePath.endsWith(".svg", ignoreCase = true)) { + return IMAGE_OK + } val millis = System.currentTimeMillis() return try { var bmp = ExifInterface(imagePath).thumbnailBitmap diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml index 0978ba8c2a2..20ca5547294 100644 --- a/app/src/main/res/layout/fragment_media_detail.xml +++ b/app/src/main/res/layout/fragment_media_detail.xml @@ -31,6 +31,14 @@ android:layout_gravity="center_horizontal" app:actualImageScaleType="none" /> + + + +