From 81bbf32b9c2bbe37bdc5c05897f87a0c9b0005af Mon Sep 17 00:00:00 2001 From: anyongjin Date: Fri, 18 Mar 2022 17:02:45 +0800 Subject: [PATCH] support setting source region to decode part of image --- README.md | 22 ++++ library/src/main/cpp/openjpg.cpp | 14 +- .../main/java/com/gemalto/jp2/JP2Decoder.java | 30 ++++- .../com/gemalto/jp2/test/MainActivity.java | 113 ++++++++++++---- testapp/src/main/res/layout/activity_main.xml | 124 +++++++++++++++++- 5 files changed, 272 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 5ab6454..645ec54 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/library/src/main/cpp/openjpg.cpp b/library/src/main/cpp/openjpg.cpp index a1d4fb6..0826e88 100644 --- a/library/src/main/cpp/openjpg.cpp +++ b/library/src/main/cpp/openjpg.cpp @@ -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 @@ -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. @@ -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; @@ -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. diff --git a/library/src/main/java/com/gemalto/jp2/JP2Decoder.java b/library/src/main/java/com/gemalto/jp2/JP2Decoder.java index 13ee33e..0f70709 100644 --- a/library/src/main/java/com/gemalto/jp2/JP2Decoder.java +++ b/library/src/main/java/com/gemalto/jp2/JP2Decoder.java @@ -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; @@ -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. @@ -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 @@ -143,8 +158,15 @@ 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); @@ -152,7 +174,7 @@ public Bitmap decode() { 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); @@ -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); } diff --git a/testapp/src/main/java/com/gemalto/jp2/test/MainActivity.java b/testapp/src/main/java/com/gemalto/jp2/test/MainActivity.java index dded00e..86b3aed 100644 --- a/testapp/src/main/java/com/gemalto/jp2/test/MainActivity.java +++ b/testapp/src/main/java/com/gemalto/jp2/test/MainActivity.java @@ -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; @@ -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) { @@ -57,17 +94,30 @@ private void close(Closeable obj) { private class DecodeJp2AsyncTask extends AsyncTask { 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 { @@ -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); } diff --git a/testapp/src/main/res/layout/activity_main.xml b/testapp/src/main/res/layout/activity_main.xml index 83a6339..3b569e3 100644 --- a/testapp/src/main/res/layout/activity_main.xml +++ b/testapp/src/main/res/layout/activity_main.xml @@ -10,11 +10,129 @@ + + + + + + + + + + + + + + + + + + + + + + + +