Skip to content

Integrating Jpegtran, remove LLJTran + Exifinterface setup#6910

Open
rovertrack wants to merge 8 commits into
commons-app:mainfrom
rovertrack:jpegtrancrop
Open

Integrating Jpegtran, remove LLJTran + Exifinterface setup#6910
rovertrack wants to merge 8 commits into
commons-app:mainfrom
rovertrack:jpegtrancrop

Conversation

@rovertrack

Copy link
Copy Markdown
Contributor

Description (required)

Integrating Jpegtran which replaces the current LLJTran + Exifinterface setup.

What changes did you make and why?

  • There were some files which were blocking the test from running as they were not meant to be instrumented android tests
    • Moved ContributionsListFragmentUnitTests.kt, MoreBottomSheetLoggedOutFragmentUnitTests.kt to unit tests
  • Replaced LLJTran + ExifInterface setup with jpegtran.
  • Moved Transformimagetests to instrumented as native C library requires instrumented android tests for testing.

Tests performed (required)

  • Transformimagetest.kt

Screenshots (for UI changes only)

…ragmentUnitTests.kt to unit tests as they are not intrumented tests
replacing LLJtran + Exifinterface setup.
@rovertrack

Copy link
Copy Markdown
Contributor Author

private fun save(exifInterface: ExifInterface?) {
try {
exifInterface?.saveAttributes()
} catch (e: IOException) {
Timber.w("EXIF redaction failed: %s", e.toString())
}

private fun copyExif(src: ExifInterface, dstFile: File) {
val dst = ExifInterface(dstFile.absolutePath)
arrayOf(
ExifInterface.TAG_DATETIME_ORIGINAL, ExifInterface.TAG_DATETIME,
ExifInterface.TAG_GPS_LATITUDE, ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE, ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_ALTITUDE, ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_DATESTAMP, ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_MAKE, ExifInterface.TAG_MODEL,
ExifInterface.TAG_F_NUMBER, ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_FLASH, ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, ExifInterface.TAG_WHITE_BALANCE,
ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.TAG_IMAGE_LENGTH,
).forEach { tag -> src.getAttribute(tag)?.let { dst.setAttribute(tag, it) } }
dst.setAttribute(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL.toString())
dst.saveAttributes()

for (tag in exifTags) {
sourceExif.setAttribute(tag, null)
}
sourceExif.saveAttributes()

@rovertrack rovertrack changed the title Jpegtrancrop Integrating Jpegtran, remove LLJTran + Exifinterface setup Jun 30, 2026
@RitikaPahwa4444 RitikaPahwa4444 requested a review from Copilot July 4, 2026 13:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the app’s JPEG edit pipeline from the previous LLJTran + ExifInterface approach to a new jpegtran-based implementation, and reorganizes tests so native-library-dependent coverage runs as instrumented tests.

Changes:

  • Replace LLJTran-based rotation/crop implementation with ajpegtran (jpegtran) and add session-style initialization/cleanup APIs.
  • Move/adjust tests: delete the old Robolectric TransformImageTest and add a new instrumented TransformImageTest; relocate some fragment tests to unit tests.
  • Update upload UI copy to reflect jpegtran usage and remove AndroidMediaUtil dependency.

Reviewed changes

Copilot reviewed 9 out of 30 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
gradle/libs.versions.toml Adds ajpegtran to the version catalog and removes AndroidMediaUtil; also introduces an unrelated XR runtime alias/version.
app/build.gradle.kts Removes AndroidMediaUtil dependency and adds ajpegtran.
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt Extends the image transform abstraction with jpegtran init/properties/cleanup APIs.
app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt Reimplements rotate/crop using jpegtran session state.
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt Adds ViewModel-level init/properties/cleanup delegation for jpegtran session handling.
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt Initializes jpegtran in the editing flow and switches crop dimension source to jpegtran properties.
app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt Updates the comment describing JPEG-only editing limitations.
app/src/test/.../ContributionsListFragmentUnitTests.kt Adds/relocates fragment unit tests.
app/src/test/.../MoreBottomSheetLoggedOutFragmentUnitTests.kt Adds/relocates fragment unit tests.
app/src/test/.../TransformImageTest.kt Removes the old Robolectric jpeg-transform test suite.
app/src/androidTest/.../TransformImageTest.kt Adds an instrumented jpegtran-backed transform test suite.
Comments suppressed due to low confidence (2)

app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt:16

  • This file has several unused imports (e.g., ExifInterface, coroutines, BufferedOutputStream, FileOutputStream) that are no longer referenced after switching from LLJTran. Removing them avoids lint failures and makes the class easier to read.
import android.content.Context
import androidx.exifinterface.media.ExifInterface
import android.net.Uri
import androidx.core.net.toUri
import fr.free.nrw.commons.ajpegtran.Jpegtran
import fr.free.nrw.commons.ajpegtran.Properties
import fr.free.nrw.commons.ajpegtran.rotate.RotationDegree
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream

app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt:44

  • This class KDoc still mentions “copying EXIF data” and “preserving its EXIF attributes”, but the explicit EXIF-copying logic was removed in this PR. The comment should be updated so it matches the current implementation (either rely on jpegtran preserving metadata, or state explicitly what is/ isn’t preserved).
 * An activity class for editing and rotating images using Jpegtran.
 *
 * This activity allows loads an image, allows users to rotate it by 90-degree increments, and
 * save the edited image while preserving its EXIF attributes. The class includes methods
 * for initializing the UI, animating image rotations, copying EXIF data, and handling
 * the image-saving process.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gradle/libs.versions.toml Outdated
Comment thread gradle/libs.versions.toml Outdated
Comment thread app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Comment thread app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Comment thread app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Comment thread app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt
Comment thread app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt
Comment thread app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt
Comment thread app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt Outdated
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

✅ Generated APK variants!

@rovertrack rovertrack marked this pull request as ready for review July 4, 2026 19:25

@RitikaPahwa4444 RitikaPahwa4444 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good in general, left some minor suggestions

} catch (e: Exception) {
try {
contentResolver.openInputStream(Uri.parse(imageUri))?.use {
contentResolver.openInputStream(imageUri.toUri())?.use {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this doesn't change the existing behaviour, please correct me if this understanding is wrong.

): File {
Timber.tag("Trying to rotate image").d("Starting")
val imagePath = System.currentTimeMillis()
val output = File(savePath, "rotated_$imagePath.jpg")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about declaring the complete image path as one variable? System.currentTimeMillis() gives a slightly different impression.

*/
interface TransformImage {

/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have a separate library, we should be able to get rid of this extra layer. I'll raise a clean-up issue that we can pick later.

* @return The cropped image File, or null if the crop operation fails.
*/
override fun cropImage(
imageFile: File,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using imageFile anywhere?

inJustDecodeBounds = true
val finalPath = file?.absolutePath
val resultIntent = Intent().apply {
putExtra("editedImageFilePath", finalPath)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can filePath be null? Not a very good way to handle it but the deleted code was sending an error back:

resultIntent.putExtra("editedImageFilePath", file?.toUri()?.path ?: "Error")
setResult(RESULT_OK, resultIntent)

@RitikaPahwa4444

Copy link
Copy Markdown
Collaborator

I just tested the app - do we need to hit the Save button on every operation? I cropped, then wanted to rotate, but that reverted the previous change... Saving exits the EditActivity, so I need to open it again to rotate.

@chrisdebian

Copy link
Copy Markdown

Nice cleanup — removing all that manual MCU-alignment/lossless-crop workaround code in favour of a proper library is a real improvement.

One behavioural difference I noticed: in the old saveEditedImage(), a rotate/crop failure returned early without calling finish(), so the user stayed on the Edit screen and could see the error Toast and retry. In the new version, finish() is in the finally block, so it runs even when the catch block just showed a failure Toast — the Edit screen closes immediately regardless of success or failure. Was that intentional, or should the failure path skip finish() (or use a longer-lived error dialog instead of a Toast that's about to get cut off by the screen closing)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants