Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,28 @@ Bitmap lowQualityBmp = new JP2Decoder(jp2data)
.decode();
```

### Source Decode Region
Sets the region of the source image that should be decoded. The region will be clipped to the
dimensions of the source image. Setting this value to null will result in the entire image
being decoded.

#### Decoding
You can obtain the width/height of image by calling
the `readHeader()` method:
```java
Header header = new JP2Decoder(jp2data).readHeader();
int imgWidth = header.width;
int imgHeight = header.height;
```

If you don't want to decode the entire image, you can set the source region to be decode.
```java
Bitmap partOfBmp = new JP2Decoder(jp2data)
.setSourceRegion(Rect(0,0,imgWidth/2,imgHeight/2))
.decode();
```


### File Format
`JP2Encoder` supports two output formats:
* JP2 - standard JPEG-2000 file format (encapsulating a JPEG-2000 codestream)
Expand Down
14 changes: 12 additions & 2 deletions library/src/main/cpp/openjpg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,8 @@ jintArray prepareReturnHeaderData(JNIEnv *env, image_header_t *outHeader) {
}

//decode a JPEG-2000 encoded file, return in 32-bit raw RGBA pixels
JNIEXPORT jintArray JNICALL Java_com_gemalto_jp2_JP2Decoder_decodeJP2File(JNIEnv *env, jclass thiz, jstring fileName, jint reduce, jint layers) {
JNIEXPORT jintArray JNICALL Java_com_gemalto_jp2_JP2Decoder_decodeJP2File(JNIEnv *env, jclass thiz, jstring fileName, jint reduce, jint layers,
jint left, jint top, jint right, jint bottom) {
opj_stream_t *l_stream = NULL; /* Stream */
opj_dparameters_t parameters; /* decompression parameters */
image_data_t outImage; //output data
Expand All @@ -1054,6 +1055,10 @@ JNIEXPORT jintArray JNICALL Java_com_gemalto_jp2_JP2Decoder_decodeJP2File(JNIEnv

parameters.decod_format = infile_format(parameters.infile);
parameters.cp_layer = layers;
parameters.DA_x0 = left;
parameters.DA_y0 = top;
parameters.DA_x1 = right;
parameters.DA_y1 = bottom;
//We don't set the reduce parameter yet, because if it's too high, it would throw an error.
//We will set it after we read the image header and find out actual number of resolutions.

Expand All @@ -1076,7 +1081,8 @@ JNIEXPORT jintArray JNICALL Java_com_gemalto_jp2_JP2Decoder_decodeJP2File(JNIEnv
}

//decode a JPEG-2000 encoded byte array, return in 32-bit raw RGBA pixels
JNIEXPORT jintArray JNICALL Java_com_gemalto_jp2_JP2Decoder_decodeJP2ByteArray(JNIEnv *env, jclass thiz, jbyteArray data, jint reduce, jint layers) {
JNIEXPORT jintArray JNICALL Java_com_gemalto_jp2_JP2Decoder_decodeJP2ByteArray(JNIEnv *env, jclass thiz, jbyteArray data,
jint reduce, jint layers, jint left, jint top, jint right, jint bottom) {
opj_stream_t *l_stream = NULL; /* Stream */
opj_dparameters_t parameters; /* decompression parameters */
char *imgData;
Expand Down Expand Up @@ -1105,6 +1111,10 @@ JNIEXPORT jintArray JNICALL Java_com_gemalto_jp2_JP2Decoder_decodeJP2ByteArray(J

parameters.decod_format = get_magic_format(imgData);
parameters.cp_layer = layers;
parameters.DA_x0 = left;
parameters.DA_y0 = top;
parameters.DA_x1 = right;
parameters.DA_y1 = bottom;
//We don't set the reduce parameter yet, because if it's too high, it would throw an error.
//We will set it after we read the image header and find out actual number of resolutions.

Expand Down
30 changes: 26 additions & 4 deletions library/src/main/java/com/gemalto/jp2/JP2Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.os.Build;
import android.util.Log;

Expand Down Expand Up @@ -42,6 +43,7 @@ public static class Header {
private int skipResolutions = 0;
private int layersToDecode = 0;
private boolean premultiplication = true;
private Rect sourceRegion = null;

/**
* Decode a JPEG-2000 image from a byte array.
Expand Down Expand Up @@ -101,6 +103,19 @@ public JP2Decoder setLayersToDecode(final int layersToDecode) {
return this;
}

/**
* Sets the region of the source image that should be decoded. The region will be clipped to the
* dimensions of the source image. Setting this value to null will result in the entire image
* being decoded.
*
* @param sourceRegion The source region to decode, or null if the entire image should be
* decoded.
*/
public void setSourceRegion(Rect sourceRegion)
{
this.sourceRegion = sourceRegion;
}

/**
* This allows you to turn off alpha pre-multiplication in the output bitmap. Normally Android bitmaps with alpha
* channel have their RGB component pre-multiplied by the normalized alpha channel. This improves performance when
Expand Down Expand Up @@ -143,16 +158,23 @@ public static boolean isJPEG2000(byte[] data) {
*/
public Bitmap decode() {
int res[] = null;
int regionLeft = 0, regionRight = 0, regionTop = 0, regionBottom = 0;
if(sourceRegion != null){
regionLeft = sourceRegion.left;
regionRight = sourceRegion.right;
regionTop = sourceRegion.top;
regionBottom = sourceRegion.bottom;
}
if (fileName != null) {
res = decodeJP2File(fileName, skipResolutions, layersToDecode);
res = decodeJP2File(fileName, skipResolutions, layersToDecode, regionLeft, regionTop, regionRight, regionBottom);
} else {
if (data == null && is != null) {
data = readInputStream(is);
}
if (data == null) {
Log.e(TAG, "Data is null, nothing to decode");
} else {
res = decodeJP2ByteArray(data, skipResolutions, layersToDecode);
res = decodeJP2ByteArray(data, skipResolutions, layersToDecode, regionLeft, regionTop, regionRight, regionBottom);
}
}
return nativeToBitmap(res);
Expand Down Expand Up @@ -245,8 +267,8 @@ private static boolean startsWith(@NonNull byte[] array1, @NonNull byte[] array2
return true;
}

private static native int[] decodeJP2File(String filename, int reduce, int layers);
private static native int[] decodeJP2ByteArray(byte[] data, int reduce, int layers);
private static native int[] decodeJP2File(String filename, int reduce, int layers, int left, int top, int right, int bottom);
private static native int[] decodeJP2ByteArray(byte[] data, int reduce, int layers, int left, int top, int right, int bottom);
private static native int[] readJP2HeaderFile(String filename);
private static native int[] readJP2HeaderByteArray(byte[] data);
}
113 changes: 91 additions & 22 deletions testapp/src/main/java/com/gemalto/jp2/test/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.gemalto.jp2.test;

import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;

import androidx.appcompat.app.AppCompatActivity;
Expand All @@ -14,24 +18,57 @@
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;

public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ImageView imgView;
private EditText infos;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

final ImageView imgView = findViewById(R.id.image);
imgView = findViewById(R.id.image);
imgView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//we want to decode the JP2 only when the layout is created and we know the ImageView size
imgView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
new DecodeJp2AsyncTask(imgView).execute();
doDecodeImg();
}
});

infos = findViewById(R.id.infos);

Button decode = findViewById(R.id.decode);
decode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
doDecodeImg();
}
});

}

private void doDecodeImg(){
EditText regionLeft = findViewById(R.id.regionLeft);
int rgLeft = Integer.parseInt(regionLeft.getText().toString());
EditText regionTop = findViewById(R.id.regionTop);
int rgTop = Integer.parseInt(regionTop.getText().toString());
EditText regionRight = findViewById(R.id.regionRight);
int rgRight = Integer.parseInt(regionRight.getText().toString());
EditText regionBottom = findViewById(R.id.regionbottom);
int rgBottom = Integer.parseInt(regionBottom.getText().toString());
Rect region = new Rect(rgLeft, rgTop, rgRight, rgBottom);

EditText skipBox = findViewById(R.id.skipResolutions);
int skipVal = Integer.parseInt(skipBox.getText().toString());
EditText layerBox = findViewById(R.id.layers);
int layerVal = Integer.parseInt(layerBox.getText().toString());

new DecodeJp2AsyncTask(imgView, region, skipVal, layerVal).execute();
}

private void close(Closeable obj) {
Expand All @@ -57,17 +94,30 @@ private void close(Closeable obj) {
private class DecodeJp2AsyncTask extends AsyncTask<Void, Void, Bitmap> {
private ImageView view;
private int width, height;
private Rect region;
private int skipVal;
private int layers;

public DecodeJp2AsyncTask(final ImageView view) {
public DecodeJp2AsyncTask(final ImageView view, Rect region, int skipVal, int layers) {
this.view = view;
//get the size of the ImageView
width = view.getWidth();
height = view.getHeight();
this.region = region;
this.skipVal = skipVal;
this.layers = layers;
}

@Override
protected Bitmap doInBackground(final Void... voids) {
Log.d(TAG, String.format("View resolution: %d x %d", width, height));
runOnUiThread(new Runnable() {
@Override
public void run() {
infos.setText("decoding...");
}
});
final StringBuilder builder = new StringBuilder();
Bitmap ret = null;
InputStream in = null;
try {
Expand All @@ -82,29 +132,48 @@ protected Bitmap doInBackground(final Void... voids) {
//get the size of the image
int imgWidth = header.width;
int imgHeight = header.height;
Log.d(TAG, String.format("JP2 resolution: %d x %d", imgWidth, imgHeight));

//we halve the resolution until we go under the ImageView size or until we run out of the available JP2 image resolutions
int skipResolutions = 1;
while (skipResolutions < header.numResolutions) {
imgWidth >>= 1;
imgHeight >>= 1;
if (imgWidth < width && imgHeight < height) break;
else skipResolutions++;
}

//we break the loop when skipResolutions goes over the correct value
skipResolutions--;
Log.d(TAG, String.format("Skipping %d resolutions", skipResolutions));

//set the number of resolutions to skip
if (skipResolutions > 0) decoder.setSkipResolutions(skipResolutions);

builder.append(String.format(Locale.US, "JP2 resolution: %d x %d, numResolutions: %d, layers: %d\n", imgWidth, imgHeight, header.numResolutions, header.numQualityLayers));

// //we halve the resolution until we go under the ImageView size or until we run out of the available JP2 image resolutions
// int skipResolutions = 1;
// while (skipResolutions < header.numResolutions) {
// imgWidth >>= 1;
// imgHeight >>= 1;
// if (imgWidth < width && imgHeight < height) break;
// else skipResolutions++;
// }
//
// //we break the loop when skipResolutions goes over the correct value
// skipResolutions--;
// Log.d(TAG, String.format("Skipping %d resolutions", skipResolutions));
// //set the number of resolutions to skip
// if (skipResolutions > 0) decoder.setSkipResolutions(skipResolutions);

decoder.setSkipResolutions(skipVal);
decoder.setLayersToDecode(layers);
decoder.setSourceRegion(region);

long start = System.currentTimeMillis();
//decode the image
ret = decoder.decode();
Log.d(TAG, String.format("Decoded at resolution: %d x %d", ret.getWidth(), ret.getHeight()));
Long cost = System.currentTimeMillis() - start;
builder.append(String.format(Locale.US, "Decoded at resolution: %d x %d, cost: %d", ret.getWidth(), ret.getHeight(), cost));
final String resText = builder.toString();
runOnUiThread(new Runnable() {
@Override
public void run() {
infos.setText(resText);
}
});
} catch (IOException e) {
e.printStackTrace();
final String errMsg = "decode fail: " + e.getMessage();
runOnUiThread(new Runnable() {
@Override
public void run() {
infos.setText(errMsg);
}
});
} finally {
close(in);
}
Expand Down
Loading