Skip to content

Library and demo app - #6

Closed
rovertrack wants to merge 3 commits into
mainfrom
dev2
Closed

rovertrack wants to merge 3 commits into
mainfrom
dev2

Conversation

@rovertrack

Copy link
Copy Markdown
Collaborator

library module :

  • Jpegtran which has its own copy of the CPP source files so no naming conflicts occurs.

  • simplified the API, the consumer just call the required features such as Crop, Blur, Rotate no need to pass in CMD commands.

  • added support for multiple blur regions, which is nothing but multiple pixelize commands firing sequentially.

  • at present only supports get_properties, Crop, Rotate, Blur.

  • every feature runs in background thread.

Demo app:

  • added Crop and blur overlay view for the feature demonstration purpose.
  • bluroverlayview has mutli region support and added functionality for deleting the selected regions through bounding boxes.
  • save function has been updated to save the latest edited file.
  • Crop, Rotate, Blur fully functional.
  • added pinch to zoom support for blur feature.

Demo:

az_recorder_20260529_020800.mp4

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Introduces a reusable jpegtran Android library that wraps the existing native jpegtran code behind a high-level Java API (getProperties, rotate, crop, blur) and adds a new commons-jpegtran-demo app that exercises that API with interactive crop/blur overlays and pinch-to-zoom. Build tooling (AGP 8.6.0, Gradle 8.7) and per-module SDK levels are also updated to support the new modules.

Changes:

  • New jpegtran library module: Jpegtran wrapper class, BlurRegion, RotationDegree, Properties, dedicated CPP copy with fr_free_nrw_commons_jpegtran_Jpegtran_* JNI names, multi-region pixelize support.
  • New commons-jpegtran-demo app with MainActivity, BlurOverlayView, CropOverlayView, plus resources/manifest, depending on :jpegtran.
  • Build upgrades: AGP 7.1.3 → 8.6.0, Gradle 7.2 → 8.7, library compileSdk 35 / minSdk 26, app namespace added, IDE files updated.

Reviewed changes

Copilot reviewed 26 out of 45 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
settings.gradle Includes the new commons-jpegtran-demo module.
build.gradle Upgrades AGP plugin to 8.6.0.
gradle/wrapper/gradle-wrapper.properties Upgrades Gradle wrapper to 8.7 with SHA-256.
jpegtran/build.gradle Library config: compile/min SDK bumps, ndkBuild path, NDK version.
jpegtran/src/main/java/.../Jpegtran.java High-level API: getProperties/rotate/crop/blur, background executor, sequential pixelize.
jpegtran/src/main/java/.../BlurRegion.java Immutable blur region data class and option-string serializer.
jpegtran/src/main/java/.../RotationDegree.java Enum of allowed rotations (90/180/270).
jpegtran/src/main/java/.../Properties.java Public JPEG properties container returned by getProperties.
commons-jpegtran-demo/build.gradle New demo app module configuration.
commons-jpegtran-demo/src/main/AndroidManifest.xml Demo app manifest.
commons-jpegtran-demo/src/main/java/.../MainActivity.java Demo flows for open/save/crop/rotate/blur using the library.
commons-jpegtran-demo/src/main/java/.../BlurOverlayView.java Overlay with pinch zoom, multi-region draw, delete-X handles, region mapping.
commons-jpegtran-demo/src/main/java/.../CropOverlayView.java Resizable crop overlay with handles, grid, and gesture exclusion.
commons-jpegtran-demo/src/main/res/* Layouts, themes, strings, colors, launcher icons.
commons-jpegtran-demo/src/{test,androidTest}/.../Example*.java Template-generated unit/instrumented tests.
app/build.gradle Adds required namespace; bumps buildToolsVersion.
.gitignore Replaces blanket .idea ignore with selective ignores.
.idea/* IDE configuration updates (JDK 17, deployment selector, etc.).
Files not reviewed (7)
  • .idea/appInsightsSettings.xml: Language not supported
  • .idea/compiler.xml: Language not supported
  • .idea/deploymentTargetSelector.xml: Language not supported
  • .idea/deviceManager.xml: Language not supported
  • .idea/gradle.xml: Language not supported
  • .idea/misc.xml: Language not supported
  • .idea/vcs.xml: Language not supported
Comments suppressed due to low confidence (1)

jpegtran/build.gradle:41

  • Several library classes import org.jspecify.annotations.NonNull / Nullable (here, Jpegtran.java, BlurRegion.java, and the overlay views) but neither jpegtran/build.gradle nor commons-jpegtran-demo/build.gradle declares an explicit org.jspecify:jspecify dependency. Compilation currently relies on it being pulled in transitively (e.g. by AppCompat's androidx.annotation); if that ever changes the build will break. The existing app module uses androidx.annotation.NonNull consistently — consider standardizing on AndroidX annotations or adding implementation 'org.jspecify:jspecify:<version>' to both modules' dependencies.

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

Comment on lines +250 to +260
jpegtran.getProperties(lloadUri, p -> {
// Display properties
TextView textView = (TextView) findViewById(R.id.text_view);
propertyStr = "File name : " + p.fileName + "\n";
propertyStr += "File size : " + p.fileSize + "\n";
propertyStr += "Width : " + p.width + "\n" + "Height : " + p.height + "\n";
propertyStr += "MCU Width : " + p.MCU_Width + "\n" + "MCU Height : " + p.MCU_Height + "\n";
propertyStr += "Color space : " + p.Color_space + "\n";
textView.setText(propertyStr);

});
Comment on lines +85 to +99
final Cursor returnCursor = resolver.query(fileUri, null, null, null, null);
final String filename;
final long filesize;

if (returnCursor != null) {
int index = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
returnCursor.moveToFirst();
filename = returnCursor.getString(index);
index = returnCursor.getColumnIndex(OpenableColumns.SIZE);
filesize = returnCursor.getLong(index);
returnCursor.close();
} else {
filename = null;
filesize = 0;
}
final int component_num = retarray[2];
final int mcu_width = retarray[3] * 8;
final int mcu_height = retarray[4] * 8;
final String[] color_space_str = {"Unknown", "Grayscale", "RGB", "YCbCr", "CMYK", "YCbCrK", "RGB", "YCbCr"};
Comment on lines +7 to +9
<string name="button_">Flip</string>
<string name="button_rotate">Rotate</string>
<string name="button_blur">blur</string>
Comment on lines +29 to +41
public int MCU_Width;
/**
* Height of one MCU block of the JPEG,
*
*/
public int MCU_Height;
/**
* Color space of the JPEG
* <p>
* Unknown, Grayscale, RGB, YCbCr, CMYK, YCbCrK, RGB, YCbCr.
*
*/
public String Color_space;
Comment on lines +307 to +309
public List<RectF> getDrawnRegions() {
return regions;
}
invalidate();
}

protected List<BlurRegion> getMappedBlurRegions(ImageView imageView, BlurOverlayView overlayView) {
Comment on lines +163 to +165
if (p != null) {
Bitmap bitmap = BitmapFactory.decodeFile(p.getAbsolutePath());
runOnUiThread(() -> {
Comment on lines +63 to +64
static Uri loadUri = null;
static String propertyStr = null;
Comment on lines +297 to +305
try {
// Workaround : To update mediastore, execute empty write.
OutputStream outstream = resolver.openOutputStream(saveUri, "wa");
outstream.flush();
outstream.close();
} catch (IOException e) {
// Discard exception
// Mediastore may not be updated, but no recovery operation and continuable.
}

@RitikaPahwa4444 RitikaPahwa4444 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Great start! The API mostly looks good. Had a few questions and suggestions, do share your thoughts on them :)

Comment thread app/build.gradle
}

android {
namespace 'github.kamemak.ajpegtran_example'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Namespace could be modified, right?

FileInputStream fis = new FileInputStream(rparcelFd.getFileDescriptor());
FileOutputStream fos = new FileOutputStream(wparcelFd.getFileDescriptor())) {
// Sequentially read and write to save uri.
byte[] buf = new byte[8192];

@RitikaPahwa4444 RitikaPahwa4444 May 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Magic number? How would the usage look like in the Commons app?

* File name of the JPEG.
*
*/
public String fileName;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should be final?

* @param post callback receiving the output file, or null on error
*/
public void crop(@NonNull Uri fileUri,
@NonNull Integer width, @NonNull Integer height,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment thread jpegtran/build.gradle

defaultConfig {
minSdk 21
minSdk 26

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let's keep this 21 only

final String color_space = (retarray[5] >= 0 && retarray[5] <= JCS_BG_YCC)
? color_space_str[retarray[5]]
: color_space_str[0]; // Unknown
handler.post(() -> post.accept(new Properties(filename, filesize, width, height, mcu_width, mcu_height, color_space)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We're not tracking component_num, is that not useful for us? We can remove it then.

Comment thread jpegtran/src/main/java/fr/free/nrw/commons/jpegtran/BlurRegion.java
Log.d(TAG, "pixelize error: " + e.getMessage());
if (tempA != null) tempA.delete();
if (tempB != null) tempB.delete();
handler.post(() -> post.accept(null));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Our callback hides the failure message from the consumer and just makes the happy path visible, is that intended? I am comparing a generic toast with displaying what exactly went wrong (consumer can decide here if the message is too technical) - the second callback would have both onSuccess() and onFailure().

final File finalTempFile = tempFile;
handler.post(() -> post.accept(finalTempFile));
} else {
tempFile.delete();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The library handles deletion of temp files on failures. Let's say I call rotate() thrice and all of them succeed. What would be the clean-up process here?

import java.util.concurrent.Executors;
import java.util.function.Consumer;

public class Jpegtran {

@RitikaPahwa4444 RitikaPahwa4444 May 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What's the responsibility of this class? I feel it's handling a lot of things. Can this be split into subclasses? How about having an interface that the consumer would interact with, and this class implementing that interface?

@nicolas-raoul

Copy link
Copy Markdown
Member

Please make file gradlew executable.

I had to modify this:

diff --git a/app/build.gradle b/app/build.gradle
index b86578d..b463455 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -8,7 +8,7 @@ android {
 
     defaultConfig {
         applicationId "github.kamemak.ajpegtran_example"
-        minSdk 19
+        minSdk 21
         targetSdk 31
         versionCode 1
         versionName "1.0"
@@ -42,7 +42,7 @@ android {
             path "src/main/cpp/Android.mk"
         }
     }
-    ndkVersion '21.4.7075529'
+    ndkVersion '29.0.13599879'
     buildToolsVersion '34.0.0'
 }

@NonNull Consumer<File> post) {

// Build options from params.
String options = "-rotate " + rotation.getDegrees() + " -optimize -copy all";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we are basically calling the library's command-line-like interface?

Is there not a direct interface that we could call? That would be better for type-checking and could allow us to drop the command-line parsing code (this could be kept as a later improvement task though).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, that's how it works in the author's ajpegtran as well!
Under the hood it's passing the argument to CLI!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

But I guess there is a function call behind the CLI, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

So you are saying modify the native C to take params instead of CLI options?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Under the hood, matching -rotate calls select_transform(JXFORM_ROT_90) etc.

The idea would be to directly call select_transform rather than using the CLI interface. I believe that would be cleaner.

@RitikaPahwa4444 Any opinion on this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

but that means we must expose that function too right!?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes.
And all of the CLI-related code could be deleted.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ok works for me if it makes the best for library :)
but Iwill also wait for one approval from @RitikaPahwa4444.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes, sounds good to me.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ok thanks Ritika

Uri tempBUri = Uri.fromFile(tempB);

// apply blur sequentially for each region in the list.
for (int i = 0; i < regions.size(); i++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there no way to perform several regions pixelizations in the same JNI call? On low-end devices blurring many regions might take time if we read/write the picture once per region.

@rovertrack rovertrack Jun 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

As far as I have tested it can take upto 3-4 at a time in the CLI version, even if we passed it ir modify the underlying C source, then too it is applied sequentially!

If less JNI call is priority, we can make the native C do the heavylifting!

@nicolas-raoul

Copy link
Copy Markdown
Member

The blur is perfect, other blocks untouched.

@rovertrack

rovertrack commented Jun 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

The blur is perfect, other blocks untouched.

Is there any particular image tester you are using?
Did you test it with the demo app?
Still i think it kind of gets big like 1px when applied I think it is automatically snapping to nearest block edge!

@nicolas-raoul

Copy link
Copy Markdown
Member

Screenshot_20260604-123432.png

@rovertrack

Copy link
Copy Markdown
Collaborator Author

Screenshot_20260604-123432.png

Thanks Nicolas :)

@nicolas-raoul

Copy link
Copy Markdown
Member

Somehow in this PR's jpegtran app when I click "Open" and select a picture, the metadata is shown but nothing else.
The strange thing is that it was working a few days agao, when I posted the result screenshot above, and I did not reinstall since then... any idea what could be wrong?

@rovertrack

Copy link
Copy Markdown
Collaborator Author

Somehow in this PR's jpegtran app when I click "Open" and select a picture, the metadata is shown but nothing else. The strange thing is that it was working a few days agao, when I posted the result screenshot above, and I did not reinstall since then... any idea what could be wrong?

Is it the upstream app or the one which was created to test out library. The details you are describing is of the app which was upstream kamemak authored! Which only opens, shows metadata and saves with hardcoded options.

@nicolas-raoul

Copy link
Copy Markdown
Member

Ah thanks Rishan, very useful, I was using the wrong app indeed! I will uninstall the upstream app.

@rovertrack

Copy link
Copy Markdown
Collaborator Author

Was pretty sure that was what happened 👍

@rovertrack

Copy link
Copy Markdown
Collaborator Author

Also it's ok if you dont test this much, this week I'm gonna be restructuring based on you recommendation of exposing methods per feature!

@nicolas-raoul

Copy link
Copy Markdown
Member

Rotation and crop also work well:

Screenshot_20260608-111355.png

Screenshot_20260608-113957.png

The above UI is https://github.com/nicolas-raoul/jpeg_diff_android

@rovertrack rovertrack closed this Jun 17, 2026
@rovertrack
rovertrack deleted the dev2 branch June 25, 2026 19:15
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.

4 participants