├── .gitignore ├── res └── balloon.jp2 ├── src └── main │ ├── resources │ └── .gitignore │ ├── c │ ├── nl_kb_jp2_JP2Reader.h │ └── nl_kb_jp2_JP2Reader.c │ └── java │ └── nl │ └── kb │ └── jp2 │ ├── JPEG2000Image.java │ └── JP2Reader.java ├── Makefile ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.idea 3 | target 4 | -------------------------------------------------------------------------------- /res/balloon.jp2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KBNLresearch/jp2view/master/res/balloon.jp2 -------------------------------------------------------------------------------- /src/main/resources/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: clean compile maven 2 | 3 | clean: 4 | rm -f src/main/resources/libkbjp2.so 5 | 6 | compile: 7 | mkdir -p src/main/resources 8 | gcc -shared -o src/main/resources/libkbjp2.so -I$(JAVA_HOME)/include/ -I$(JAVA_HOME)/include/linux/ -I${OPJ_INC} src/main/c/nl_kb_jp2_JP2Reader.c -fPIC -lopenjp2 9 | 10 | maven: 11 | mvn clean install package assembly:single 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jp2view 2 | ======= 3 | 4 | first version, tested on [openjpeg2.0](http://code.google.com/p/openjpeg/downloads/detail?name=openjpeg-2.0.0.tar.gz&can=2&q=) with openjdk1.7 5 | 6 | Build and install openjpeg 7 | ----- 8 | 9 | 1. cd /your/path/to/openjpeg2.0 10 | 11 | 2. sudo apt get install cmake make 12 | 13 | 3. cmake . 14 | 15 | 4. make 16 | 17 | 5. sudo make install 18 | 19 | 6. sudo ldconfig (alternatively set LD_LIBRARY_PATH to location of libopenjp2.so) 20 | 21 | Build this project 22 | ---- 23 | 24 | (prerequisites: make and maven2+) 25 | 26 | 1. cd /your/path/to/jp2view 27 | 28 | 2. export OPJ_INC=/your/includepath/openjpeg2.0 (i.e.: /usr/local/include/openjpeg-2.0/) 29 | 30 | 3. export JAVA_HOME=/your/jdkpath (i.e.: /usr/lib/jvm/java-1.7.0-openjdk-amd64) 31 | 32 | 4. make 33 | 34 | 5. cd target 35 | 36 | 6. java -jar jp2-0.1.0-jar-with-dependencies.jar nl.kb.JP2Reader ../res/balloon.jp2 4 37 | 38 | If all went well, there are now 5 jpeg files in your target directory: 39 | - test.jpg file (full res balloon) 40 | - test_region.jpg 41 | - test_region_scaled_up.jpg 42 | - test_region_scaled_down.jpg 43 | - test_full_scaled.jpg 44 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | nl.kb 8 | jp2 9 | 0.1.0 10 | jar 11 | 12 | 13 | 14 | 15 | commons-io 16 | commons-io 17 | 2.7 18 | 19 | 20 | 21 | 22 | 23 | 24 | maven-assembly-plugin 25 | 26 | 27 | 28 | nl.kb.jp2.JP2Reader 29 | 30 | 31 | 32 | jar-with-dependencies 33 | 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-compiler-plugin 39 | 2.5.1 40 | 41 | true 42 | true 43 | 1.7 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/c/nl_kb_jp2_JP2Reader.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Koninklijke Bibliotheek - Nationale bibliotheek van Nederland 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without modification, 7 | * are permitted provided that the following conditions are met: 8 | * 9 | * * Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * * Neither the name of the Koninklijke Bibliotheek nor the names of its contributors 15 | * may be used to endorse or promote products derived from this software without 16 | * specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 22 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | * 30 | */ 31 | 32 | #include 33 | 34 | #ifndef _Included_nl_kb_jp2_JP2Reader 35 | #define _Included_nl_kb_jp2_JP2Reader 36 | #ifdef __cplusplus 37 | extern "C" { 38 | #endif 39 | 40 | /* 41 | * Class: nl_kb_jp2_JP2Reader 42 | * Method: getJp2Specs 43 | * Signature: (Ljava/lang/String;)[I 44 | */ 45 | JNIEXPORT jintArray JNICALL Java_nl_kb_jp2_JP2Reader_getJp2Specs 46 | (JNIEnv *, jclass, jstring); 47 | 48 | /* 49 | * Class: nl_kb_jp2_JP2Reader 50 | * Method: getJp2Specs 51 | * Signature: (Ljava/lang/String;)[I 52 | */ 53 | JNIEXPORT jintArray JNICALL JNICALL Java_nl_kb_jp2_JP2Reader_getTile 54 | (JNIEnv *, jclass, jstring, jint, jint, jobjectArray); 55 | 56 | 57 | #ifdef __cplusplus 58 | } 59 | #endif 60 | #endif 61 | -------------------------------------------------------------------------------- /src/main/java/nl/kb/jp2/JPEG2000Image.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Koninklijke Bibliotheek - Nationale bibliotheek van Nederland 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without modification, 7 | * are permitted provided that the following conditions are met: 8 | * 9 | * * Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * * Neither the name of the Koninklijke Bibliotheek nor the names of its contributors 15 | * may be used to endorse or promote products derived from this software without 16 | * specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 22 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | * 30 | */ 31 | 32 | package nl.kb.jp2; 33 | 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | 37 | public class JPEG2000Image { 38 | 39 | public static final int HEADER_FAIL = 0; 40 | public static final int HEADER_SUCCESS = 1; 41 | private int status; 42 | private int width; 43 | private int height; 44 | private int tilesX; 45 | private int tilesY; 46 | private int tileW; 47 | private int tileH; 48 | private int maxReduction; 49 | private int numCompositions; 50 | private String filename; 51 | 52 | public JPEG2000Image(String filename, final int[] arySpex) { 53 | this.filename = filename; 54 | status = arySpex[0]; 55 | width = arySpex[1]; 56 | height = arySpex[2]; 57 | tilesX = arySpex[3]; 58 | tilesY = arySpex[4]; 59 | tileW = arySpex[5]; 60 | tileH = arySpex[6]; 61 | maxReduction = arySpex[7]; 62 | numCompositions = arySpex[8]; 63 | } 64 | 65 | public boolean headerLoaded() { 66 | return status == HEADER_SUCCESS; 67 | } 68 | 69 | public int getBestReductionFactorForScale(double scale) { 70 | int redux = maxReduction - 1; 71 | double scaleAtRedux = reduce(1.0, redux); 72 | while(scale > scaleAtRedux && redux > 0) { 73 | scaleAtRedux *= 2.0; 74 | redux--; 75 | } 76 | 77 | return redux; 78 | } 79 | 80 | public int getTilesX() { 81 | return tilesX; 82 | } 83 | 84 | public int getTilesY() { 85 | return tilesY; 86 | } 87 | 88 | public int getNumCompositions() { 89 | return numCompositions; 90 | } 91 | 92 | public int getMaxReduction() { 93 | return maxReduction - 1; 94 | } 95 | 96 | public static double reduce(double num, int reduction) { 97 | for(int i = 0; i < reduction; ++i) { 98 | num /= 2; 99 | } 100 | return num; 101 | } 102 | 103 | public static int reduce(int num, int reduction) { 104 | for(int i = 0; i < reduction; ++i) { 105 | num = (int) Math.ceil(((double) num) / 2.0d); 106 | } 107 | return num; 108 | } 109 | 110 | public int getWidth(int reduction) { 111 | return reduce(width, reduction); 112 | } 113 | 114 | public int getHeight(int reduction) { 115 | return reduce(height, reduction); 116 | } 117 | 118 | public int getTileW(int reduction) { 119 | return reduce(tileW, reduction); 120 | } 121 | 122 | public int getTileH(int reduction) { 123 | return reduce(tileH, reduction); 124 | } 125 | 126 | public String getFilename() { 127 | return filename; 128 | } 129 | 130 | private List filterTilesDim(int start, int finish, int tiles, int tsiz, int reduction) { 131 | int siz = reduce(tsiz, reduction); 132 | List indices = new ArrayList(); 133 | for(int i = 0; i < tiles; ++i) { 134 | int cur1 = i * siz; 135 | int cur2 = (i+1) * siz; 136 | if( (start >= cur1 && start <= cur2) || 137 | (finish >= cur1 && finish <= cur2) || 138 | (start <= cur1 && finish >= cur2)) { 139 | indices.add(i); 140 | } 141 | } 142 | return indices; 143 | } 144 | 145 | public List filterTilesX(int x, int w, int reduction) { 146 | return filterTilesDim(x, x + w, tilesX, tileW, reduction); 147 | } 148 | 149 | public List filterTilesY(int y, int h, int reduction) { 150 | return filterTilesDim(y, y + h, tilesY, tileH, reduction); 151 | } 152 | 153 | 154 | @Override 155 | public String toString() { 156 | return "JPEG2000Image{" + 157 | "status=" + (status == HEADER_FAIL ? "HEADER_FAIL" : "HEADER_SUCCESS") + 158 | ", width=" + width + 159 | ", height=" + height + 160 | ", tilesX=" + tilesX + 161 | ", tilesY=" + tilesY + 162 | ", tileW=" + tileW + 163 | ", tileH=" + tileH + 164 | ", maxReduction=" + maxReduction + 165 | ", numCompositions=" + numCompositions + 166 | '}'; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/main/c/nl_kb_jp2_JP2Reader.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Koninklijke Bibliotheek - Nationale bibliotheek van Nederland 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without modification, 7 | * are permitted provided that the following conditions are met: 8 | * 9 | * * Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * * Neither the name of the Koninklijke Bibliotheek nor the names of its contributors 15 | * may be used to endorse or promote products derived from this software without 16 | * specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 22 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | * 30 | */ 31 | #include 32 | #include 33 | #include "nl_kb_jp2_JP2Reader.h" 34 | 35 | static void error_callback(const char *msg, void *client_data) {(void)client_data; fprintf(stdout, "[ERROR] %s\r\n", msg);} 36 | static void warning_callback(const char *msg, void *client_data) { (void)client_data; fprintf(stdout, "[WARNING] %s\r\n", msg);} 37 | static void info_callback(const char *msg, void *client_data) {(void)client_data; fprintf(stdout, "[INFO] %s\r\n", msg);} 38 | 39 | struct opj_res { 40 | int status; 41 | opj_stream_t *l_stream; 42 | opj_codec_t *l_codec; 43 | opj_image_t *image; 44 | FILE * open_file; 45 | }; 46 | 47 | struct opj_res opj_init(const char *fname, opj_dparameters_t *parameters) { 48 | 49 | struct opj_res resources; 50 | resources.status = 0; 51 | resources.image = NULL; 52 | FILE *fptr = fopen(fname, "rb"); 53 | resources.open_file = fptr; 54 | resources.l_stream = opj_stream_create_default_file_stream(fptr,1); 55 | resources.l_codec = opj_create_decompress(OPJ_CODEC_JP2); 56 | if(!resources.l_stream) { resources.status = 1; } 57 | if(!opj_setup_decoder(resources.l_codec, parameters)) { 58 | opj_stream_destroy(resources.l_stream); 59 | opj_destroy_codec(resources.l_codec); 60 | resources.status = 2; 61 | } 62 | 63 | if(!opj_read_header(resources.l_stream, resources.l_codec, &(resources.image))) { 64 | opj_stream_destroy(resources.l_stream); 65 | opj_destroy_codec(resources.l_codec); 66 | opj_image_destroy(resources.image); 67 | resources.status = 3; 68 | } 69 | 70 | /* opj_set_info_handler(resources.l_codec, info_callback,00); 71 | opj_set_warning_handler(resources.l_codec, warning_callback,00); 72 | opj_set_error_handler(resources.l_codec, error_callback,00);*/ 73 | return resources; 74 | } 75 | 76 | void opj_cleanup(struct opj_res *resources) { 77 | if(resources->l_stream) { opj_stream_destroy(resources->l_stream); } 78 | if(resources->l_codec) { opj_destroy_codec(resources->l_codec); } 79 | if(resources->image) { opj_image_destroy(resources->image); } 80 | if(resources->open_file) { fclose(resources->open_file); } 81 | } 82 | 83 | 84 | 85 | #define JP2_RFC3745_MAGIC "\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a" 86 | #define JP2_MAGIC "\x0d\x0a\x87\x0a" 87 | static int is_jp2(FILE *fptr) { 88 | unsigned char buf[12]; 89 | unsigned int l_nb_read; 90 | 91 | l_nb_read = fread(buf, 1, 12, fptr); 92 | fseek(fptr, 0, SEEK_SET); 93 | 94 | int retval = memcmp(buf, JP2_RFC3745_MAGIC, 12) == 0 || memcmp(buf, JP2_MAGIC, 4) == 0; 95 | fclose(fptr); 96 | return retval; 97 | } 98 | 99 | #define READ_FAILURE 0 100 | #define READ_SUCCESS 1 101 | 102 | 103 | JNIEXPORT jintArray JNICALL Java_nl_kb_jp2_JP2Reader_getTile 104 | (JNIEnv *env, jclass cls, jstring fname, jint tile_index, jint reduction_factor, jobjectArray pixels) { 105 | const char *filename = (*env)->GetStringUTFChars(env, fname, 0); 106 | jclass intArrayClass = (*env)->FindClass(env, "[I"); 107 | jintArray ary = (*env)->NewIntArray(env, 3); 108 | 109 | int data[3]; 110 | int i = 0; 111 | for(i = 0; i < 3; ++i) { data[i] = 0; } 112 | data[0] = READ_FAILURE; 113 | 114 | FILE *fptr = fopen(filename, "rb"); 115 | if(fptr != NULL && is_jp2(fptr)) { 116 | opj_dparameters_t parameters; 117 | opj_set_default_decoder_parameters(¶meters); 118 | parameters.cp_reduce = reduction_factor; 119 | parameters.cp_layer = 100; 120 | struct opj_res resources = opj_init(filename, ¶meters); 121 | if(resources.status == 0 && opj_get_decoded_tile(resources.l_codec, resources.l_stream, resources.image, tile_index)) { 122 | int numpix = resources.image->comps[0].w * resources.image->comps[0].h; 123 | int comp; 124 | for(comp = 0; comp < resources.image->numcomps; ++comp) { 125 | jintArray data = (*env)->NewIntArray(env, numpix); 126 | (*env)->SetIntArrayRegion(env, data, (jsize) 0, (jsize) numpix, (jint*) resources.image->comps[comp].data); 127 | (*env)->SetObjectArrayElement(env, pixels, (jsize) comp, data); 128 | } 129 | data[0] = READ_SUCCESS; 130 | data[1] = resources.image->comps[0].w; 131 | data[2] = resources.image->comps[0].h; 132 | } 133 | opj_cleanup(&resources); 134 | } 135 | 136 | (*env)->SetIntArrayRegion(env, ary, 0, 3, data); 137 | return ary; 138 | } 139 | 140 | 141 | 142 | #define FIELD_LEN 9 143 | JNIEXPORT jintArray JNICALL Java_nl_kb_jp2_JP2Reader_getJp2Specs 144 | (JNIEnv *env, jclass cls, jstring fname) { 145 | 146 | const char *filename = (*env)->GetStringUTFChars(env, fname, 0); 147 | jintArray ary = (*env)->NewIntArray(env, FIELD_LEN); 148 | 149 | int data[FIELD_LEN]; 150 | int i = 0; 151 | for(i = 0; i < FIELD_LEN; ++i) { data[i] = 0; } 152 | data[0] = READ_FAILURE; 153 | 154 | FILE *fptr = fopen(filename, "rb"); 155 | if(fptr != NULL && is_jp2(fptr)) { 156 | opj_dparameters_t parameters; 157 | opj_set_default_decoder_parameters(¶meters); 158 | struct opj_res resources = opj_init(filename, ¶meters); 159 | 160 | opj_codestream_info_v2_t* info = opj_get_cstr_info(resources.l_codec); 161 | if(resources.status == 0) { 162 | data[0] = READ_SUCCESS; 163 | data[1] = resources.image->x1; 164 | data[2] = resources.image->y1; 165 | data[3] = info->tw; 166 | data[4] = info->th; 167 | data[5] = info->tdx; 168 | data[6] = info->tdy; 169 | data[7] = info->m_default_tile_info.tccp_info[0].numresolutions; 170 | data[8] = resources.image->numcomps; 171 | } 172 | opj_destroy_cstr_info(&info); 173 | opj_cleanup(&resources); 174 | } else { 175 | error_callback("Cannot read file:", NULL); 176 | error_callback(filename, NULL); 177 | } 178 | (*env)->SetIntArrayRegion(env, ary, 0, FIELD_LEN, data); 179 | return ary; 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/nl/kb/jp2/JP2Reader.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Koninklijke Bibliotheek - Nationale bibliotheek van Nederland 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without modification, 7 | * are permitted provided that the following conditions are met: 8 | * 9 | * * Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * * Neither the name of the Koninklijke Bibliotheek nor the names of its contributors 15 | * may be used to endorse or promote products derived from this software without 16 | * specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 22 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | * 30 | */ 31 | 32 | package nl.kb.jp2; 33 | 34 | import org.apache.commons.io.IOUtils; 35 | 36 | import javax.imageio.ImageIO; 37 | import java.awt.*; 38 | import java.awt.image.BufferedImage; 39 | import java.io.File; 40 | import java.io.FileOutputStream; 41 | import java.io.IOException; 42 | import java.io.InputStream; 43 | import java.util.ArrayList; 44 | import java.util.Date; 45 | import java.util.List; 46 | 47 | 48 | public class JP2Reader { 49 | private static final String LIBRARY_NAME = "libkbjp2.so"; 50 | private static final int MAX_THREADS_PER_JOB = 8; 51 | 52 | 53 | static { 54 | InputStream is = JP2Reader.class.getResourceAsStream("/" + LIBRARY_NAME); 55 | try { 56 | File temp = File.createTempFile(LIBRARY_NAME, ""); 57 | FileOutputStream fos = new FileOutputStream(temp); 58 | fos.write(IOUtils.toByteArray(is)); 59 | fos.close(); 60 | is.close(); 61 | System.load(temp.getAbsolutePath()); 62 | } catch(IOException e) { 63 | throw new RuntimeException(e); 64 | } 65 | } 66 | 67 | public native int[] getJp2Specs(String filename); 68 | private native int[] getTile(String filename, int tileIndex, int reduction, int[][] pixels); 69 | 70 | 71 | private class TileToBufferJob implements Runnable { 72 | private BufferedImage img; 73 | private JPEG2000Image image; 74 | private int reduction; 75 | private int tileIndex; 76 | private int imageX; 77 | private int imageY; 78 | 79 | private int subX1 = -1; 80 | private int subY1 = -1; 81 | private int subX = -1; 82 | private int subY = -1; 83 | private int realX = 0; 84 | private int realY = 0; 85 | 86 | public TileToBufferJob(JPEG2000Image image, BufferedImage img, int reduction, int tileIndex, int imageX, int imageY, 87 | int subX, int subY, int subX1, int subY1, int realX, int realY) { 88 | this(image, img, reduction, tileIndex, imageX, imageY); 89 | this.subX = subX; 90 | this.subY = subY; 91 | this.subX1 = subX1; 92 | this.subY1 = subY1; 93 | this.realX = realX; 94 | this.realY = realY; 95 | } 96 | 97 | public TileToBufferJob(JPEG2000Image image, BufferedImage img, int reduction, int tileIndex, int imageX, int imageY) { 98 | this.image = image; 99 | this.img = img; 100 | this.reduction = reduction; 101 | this.tileIndex = tileIndex; 102 | this.imageX = imageX; 103 | this.imageY = imageY; 104 | } 105 | 106 | public void run() { 107 | int[][] tileRBG = new int[image.getNumCompositions()][]; 108 | int[] tileSpecs = getTile(image.getFilename(), tileIndex, reduction, tileRBG); 109 | int startX = subX > 0 ? subX : 0; 110 | int startY = subY > 0 ? subY : 0; 111 | int endX = subX1 > 0 ? (subX1 > tileSpecs[1] ? tileSpecs[1] : subX1) : tileSpecs[1]; 112 | int endY = subY1 > 0 ? (subY1 > tileSpecs[2] ? tileSpecs[2] : subY1) : tileSpecs[2]; 113 | int outLeft = imageX - realX < 0 ? 0 : imageX - realX; 114 | int outTop = imageY - realY < 0 ? 0 : imageY - realY; 115 | 116 | for(int y = startY; y < endY; ++y) { 117 | for(int x = startX; x < endX; ++x) { 118 | int[] rgb = new int[3]; 119 | int i = y * tileSpecs[1] + x; 120 | if(image.getNumCompositions() >= 3) { 121 | rgb[0] = tileRBG[0][i]; 122 | rgb[1] = tileRBG[1][i]; 123 | rgb[2] = tileRBG[2][i]; 124 | } else { 125 | /** we do not use the alpha channel at the kbnl; maybe grayscale though **/ 126 | rgb[0] = tileRBG[0][i]; 127 | rgb[1] = tileRBG[0][i]; 128 | rgb[2] = tileRBG[0][i]; 129 | } 130 | try { 131 | img.getRaster().setPixel(outLeft + x - startX, outTop + y - startY, rgb); 132 | } catch(ArrayIndexOutOfBoundsException e) { 133 | System.err.print("x"); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | 141 | /** 142 | * Get the full JP2 image at requested at the requested reduction level. 143 | * @param image the image object representing the image 144 | * @param reduction the requested reduction level (will be cut off at max reduction level) 145 | * @return buffered image object containing the full image 146 | */ 147 | public BufferedImage getFullImage(JPEG2000Image image, int reduction) { 148 | if(reduction < 0) { reduction = 0; } 149 | if(reduction > image.getMaxReduction()) { reduction = image.getMaxReduction(); } 150 | return getRegion(image, reduction, 0, 0, image.getWidth(reduction), image.getHeight(reduction)); 151 | } 152 | 153 | /** 154 | * Get the full JP2 image at requested scale. 155 | * @param image the image object representing the image 156 | * @param scale the requested rescale factor 157 | * @return buffered image object containing the full image 158 | */ 159 | public BufferedImage getFullImage(JPEG2000Image image, double scale) { 160 | return getRegion(image, scale, 0, 0, (int) Math.ceil(image.getWidth(0) * scale), (int) Math.ceil(image.getHeight(0) * scale)); 161 | } 162 | 163 | /** 164 | * Get a region of a JP2 image at the requested scale. 165 | * @param image the image object representing the image 166 | * @param scale the requested rescale factor 167 | * @param x the x-position on the image after resize to scale 168 | * @param y the y-position on the image after resize to scale 169 | * @param w the width after resize 170 | * @param h the height after resize 171 | * @return buffered image object containing the selected region 172 | */ 173 | public BufferedImage getRegion(JPEG2000Image image, double scale, int x, int y, int w, int h) { 174 | int reduction = image.getBestReductionFactorForScale(scale); 175 | double jp2scale = JPEG2000Image.reduce(1.0, reduction); 176 | double factor = jp2scale / scale; 177 | int x1 = (int) Math.ceil(factor * (double)x); 178 | int y1 = (int) Math.ceil(factor * (double)y); 179 | int w1 = (int) Math.ceil(factor * (double)w); 180 | int h1 = (int) Math.ceil(factor * (double)h); 181 | BufferedImage img = getRegion(image, reduction, x1, y1, w1, h1); 182 | BufferedImage out = new BufferedImage(w, h, img.getType()); 183 | Graphics2D g = out.createGraphics(); 184 | g.drawImage(img, 0, 0, w, h, null); 185 | g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR); 186 | g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY); 187 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); 188 | 189 | return out; 190 | } 191 | 192 | 193 | /** 194 | * Get a region of a JP2 image at a supported resolution reduction level. 195 | * @param image the image object representing the image (TODO: check for successful load) 196 | * @param reduction the requested reduction level (will be cut off at max reduction level) 197 | * @param x the x-position on the image after resize to reduction level 198 | * @param y the y-position on the image after resize to reduction level 199 | * @param w the width after resize 200 | * @param h the height after resize 201 | * @return buffered image object containing the selected region 202 | */ 203 | public BufferedImage getRegion(JPEG2000Image image, int reduction, int x, int y, int w, int h) { 204 | if(reduction < 0) { reduction = 0; } 205 | else if(reduction > image.getMaxReduction()) { reduction = image.getMaxReduction(); } 206 | if(x > image.getWidth(reduction)) { x = image.getWidth(reduction); } 207 | if(y > image.getHeight(reduction)) { y = image.getHeight(reduction); } 208 | if(x + w > image.getWidth(reduction)) { w = image.getWidth(reduction) - x; } 209 | if(y + h > image.getHeight(reduction)) { h = image.getHeight(reduction) - y; } 210 | 211 | if(w <= 0|| h <= 0) { 212 | return new BufferedImage(1,1, BufferedImage.TYPE_INT_RGB); 213 | } 214 | 215 | BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); 216 | 217 | List openThreads = new ArrayList(); 218 | for(int tileX : image.filterTilesX(x, w, reduction)) { 219 | int imageX = tileX * image.getTileW(reduction); 220 | for(int tileY : image.filterTilesY(y, h, reduction)) { 221 | int imageY = tileY * image.getTileH(reduction); 222 | int tileIndex = (image.getTilesX() * tileY) + tileX; 223 | int subX = (imageX > x ? 0 : x - imageX); 224 | int subY = (imageY > y ? 0 : y - imageY); 225 | int subX1 = (imageX + image.getTileW(reduction) > x + w ? 226 | (x + w) - imageX : 227 | image.getTileW(reduction)); 228 | int subY1 = (imageY + image.getTileH(reduction) > y + h ? 229 | (y + h) - imageY : 230 | image.getTileH(reduction)); 231 | 232 | TileToBufferJob job = new TileToBufferJob(image, img, reduction, tileIndex, imageX, imageY, 233 | subX, subY, subX1, subY1, x, y); 234 | Thread t = new Thread(job); 235 | t.start(); 236 | openThreads.add(t); 237 | if(openThreads.size() >= MAX_THREADS_PER_JOB) { 238 | try { 239 | openThreads.remove(0).join(); 240 | } catch(InterruptedException e) { } 241 | } 242 | } 243 | } 244 | for(Thread t : openThreads) { 245 | try { 246 | t.join(); 247 | } catch (InterruptedException e) { } 248 | } 249 | 250 | return img; 251 | } 252 | 253 | /** 254 | * TODO: move to unit tests, parse the command line 255 | * @param args 256 | */ 257 | public static void main(String args[]) { 258 | String filename = args[1]; 259 | long start = new Date().getTime(); 260 | JP2Reader reader = new JP2Reader(); 261 | 262 | JPEG2000Image image = new JPEG2000Image(filename, reader.getJp2Specs(filename)); 263 | System.out.println("Read header ms: " + ((new Date().getTime()) - start)); 264 | if(image.headerLoaded()) { 265 | System.out.println(image); 266 | 267 | start = new Date().getTime(); 268 | BufferedImage outImg = reader.getFullImage(image, 0); 269 | System.out.println("Decompile full image no reduction ms: " + ((new Date().getTime()) - start)); 270 | try { 271 | ImageIO.write(outImg, "jpg", new File("test.jpg")); 272 | } catch(IOException e) { 273 | e.printStackTrace(); 274 | } 275 | 276 | start = new Date().getTime(); 277 | BufferedImage outImg1 = reader.getRegion(image, 1, 250, 135, 180, 400); 278 | System.out.println("Get region ms: " + ((new Date().getTime()) - start)); 279 | try { 280 | ImageIO.write(outImg1, "jpg", new File("test_region.jpg")); 281 | } catch(IOException e) { 282 | e.printStackTrace(); 283 | } 284 | 285 | start = new Date().getTime(); 286 | BufferedImage outImg2 = reader.getRegion(image, 0.5d, 250, 135, 180, 400); 287 | System.out.println("Get scaled DOWN region ms: " + ((new Date().getTime()) - start)); 288 | try { 289 | ImageIO.write(outImg2, "jpg", new File("test_region_scaled.jpg")); 290 | } catch(IOException e) { 291 | e.printStackTrace(); 292 | } 293 | 294 | start = new Date().getTime(); 295 | BufferedImage outImg4 = reader.getRegion(image, 1.5d, 0, 0, 1500, 800); 296 | System.out.println("Get scaled UP region ms: " + ((new Date().getTime()) - start)); 297 | try { 298 | ImageIO.write(outImg4, "jpg", new File("test_region_scaled_up.jpg")); 299 | } catch(IOException e) { 300 | e.printStackTrace(); 301 | } 302 | 303 | start = new Date().getTime(); 304 | BufferedImage outImg3 = reader.getFullImage(image, 0.01d); 305 | System.out.println("Get scaled full image (s=0.01) ms: " + ((new Date().getTime()) - start)); 306 | try { 307 | ImageIO.write(outImg3, "jpg", new File("test_full_scaled.jpg")); 308 | } catch(IOException e) { 309 | e.printStackTrace(); 310 | } 311 | 312 | } else { 313 | System.err.println("failed to load file: " + filename); 314 | } 315 | } 316 | } 317 | --------------------------------------------------------------------------------