/*******************************************************************************
* Copyright (c) 2016 Weasis Team and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Nicolas Roduit - initial API and implementation
*******************************************************************************/
package org.weasis.openjpeg.internal;
import java.awt.Rectangle;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageWriteParam;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import org.bytedeco.javacpp.IntPointer;
import org.bytedeco.javacpp.Pointer;
import org.bytedeco.javacpp.SizeTPointer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.weasis.image.jni.ImageParameters;
import org.weasis.image.jni.NativeCodec;
import org.weasis.image.jni.NativeImage;
import org.weasis.image.jni.StreamSegment;
import org.weasis.openjpeg.J2kParameters;
import org.weasis.openjpeg.NativeJ2kImage;
import org.weasis.openjpeg.cpp.openjpeg;
import org.weasis.openjpeg.cpp.openjpeg.SourceData;
import org.weasis.openjpeg.cpp.openjpeg.error_handler;
import org.weasis.openjpeg.cpp.openjpeg.info_handler;
import org.weasis.openjpeg.cpp.openjpeg.opj_image;
import org.weasis.openjpeg.cpp.openjpeg.opj_image_comp;
import org.weasis.openjpeg.cpp.openjpeg.warning_handler;
public class OpenJpegCodec implements NativeCodec {
public static final Logger LOGGER = LoggerFactory.getLogger(OpenJpegCodec.class);
public static final int J2K_CFMT = 0;
public static final int JP2_CFMT = 1;
public static final int JPT_CFMT = 2;
static final info_handler infoHandler = new info_handler();
static final warning_handler warningHandler = new warning_handler();
static final error_handler errorHandler = new error_handler();
/** 1 mega of buffersize by default */
public static final long OPJ_J2K_STREAM_CHUNK_SIZE = 0x100000;
@Override
public String readHeader(NativeImage nImage) throws IOException {
String msg = null;
StreamSegment seg = nImage.getStreamSegment();
if (seg != null) {
J2kParameters params = (J2kParameters) nImage.getImageParameters();
Pointer lstream = null;
Pointer codec = null;
openjpeg.opj_image image = null;
try {
ByteBuffer buffer = seg.getDirectByteBuffer(0);
SourceData j2kFile = new SourceData();
j2kFile.data(buffer);
SizeTPointer size = new SizeTPointer(1);
size.put(buffer.limit());
j2kFile.size(size);
lstream = openjpeg.opj_stream_create_memory_stream(j2kFile, 0x10000, true); // 65536 bytes
if (lstream.isNull()) {
throw new IOException("Cannot initialize stream!");
}
codec = getCodec(params.getType());
if (codec.isNull()) {
throw new IOException("No j2k decoder for this type: " + params.getType());
}
/* catch events using our callbacks and give a local context */
openjpeg.opj_set_info_handler(codec, infoHandler, null);
openjpeg.opj_set_warning_handler(codec, warningHandler, null);
openjpeg.opj_set_error_handler(codec, errorHandler, null);
/* setup the decoder decoding parameters using user parameters */
/* Read the main header of the codestream and if necessary the JP2 boxes */
openjpeg.opj_dparameters parameters = new openjpeg.opj_dparameters();
openjpeg.opj_set_default_decoder_parameters(parameters);
if (!openjpeg.opj_setup_decoder(codec, parameters)) {
throw new IOException("Failed to setup the decoder");
}
/* Read the main header of the codestream and if necessary the JP2 boxes */
image = new openjpeg.opj_image();
if (!openjpeg.opj_read_header(lstream, codec, image)) {
throw new IOException("Failed to read the j2k header");
}
setParameters(nImage.getImageParameters(), image);
// keep a reference to be not garbage collected
buffer.clear();
j2kFile.deallocate();
} finally {
if (lstream != null) {
openjpeg.opj_stream_destroy(lstream);
lstream.deallocate();
}
if (codec != null) {
openjpeg.opj_destroy_codec(codec);
codec.deallocate();
}
if (image != null) {
openjpeg.opj_image_destroy(image);
image.deallocate();
}
// Do not close inChannel (comes from image input stream)
}
}
return msg;
}
@Override
public String decompress(NativeImage nImage, ImageReadParam param) throws IOException {
String msg = null;
StreamSegment seg = nImage.getStreamSegment();
if (seg != null) {
Pointer lstream = null;
Pointer codec = null;
openjpeg.opj_image image = null;
try {
// When multiple fragments segments, aggregate them in the byteBuffer.
ByteBuffer buffer = seg.getDirectByteBuffer(0, seg.getSegLength().length - 1);
// TODO apply signed at DICOM level?
// boolean signed = params.isSignedData();
J2kParameters j2kparams = (J2kParameters) nImage.getImageParameters();
SourceData j2kFile = new SourceData();
j2kFile.data(buffer);
SizeTPointer srcDataSize = new SizeTPointer(1);
srcDataSize.put(buffer.limit());
j2kFile.size(srcDataSize);
lstream = openjpeg.opj_stream_create_memory_stream(j2kFile, OPJ_J2K_STREAM_CHUNK_SIZE, true);
if (lstream.isNull()) {
throw new IOException("Cannot initialize stream!");
}
codec = getCodec(j2kparams.getType());
if (codec.isNull()) {
throw new IOException("No j2k decoder for this type: " + j2kparams.getType());
}
/* catch events using our callbacks and give a local context */
openjpeg.opj_set_info_handler(codec, infoHandler, null);
openjpeg.opj_set_warning_handler(codec, warningHandler, null);
openjpeg.opj_set_error_handler(codec, errorHandler, null);
/* setup the decoder decoding parameters using user parameters */
/* Read the main header of the codestream and if necessary the JP2 boxes */
openjpeg.opj_dparameters parameters = new openjpeg.opj_dparameters();
openjpeg.opj_set_default_decoder_parameters(parameters);
parameters.decod_format(j2kparams.getType());
parameters.cp_layer(0);
parameters.cp_reduce(0);
if (!openjpeg.opj_setup_decoder(codec, parameters)) {
throw new IOException("Failed to setup the decoder");
}
/* Read the main header of the codestream and if necessary the JP2 boxes */
image = new openjpeg.opj_image();
if (!openjpeg.opj_read_header(lstream, codec, image)) {
throw new IOException("Failed to read the j2k header");
}
setParameters(nImage.getImageParameters(), image);
int bps = j2kparams.getBitsPerSample();
if (bps < 1 || bps > 16) {
throw new IllegalArgumentException("Invalid bit per sample: " + bps);
}
Rectangle area = param.getSourceRegion();
// Rectangle area = new Rectangle();
// area.width = j2kparams.getWidth();
// area.height = j2kparams.getHeight();
/* Do not decode the entire image if are is not null */
if (area != null && !openjpeg.opj_set_decode_area(codec, image, area.x, area.y, area.x + area.width,
area.y + area.height)) {
throw new IOException("Failed to set the decoded area!");
}
// TODO need to be tested
// if (parameters.nb_tile_to_decode() > 0) {
// ByteBuffer outBuf = null;
// BoolPointer l_go_on = new BoolPointer(1);
// l_go_on.put(true);
// IntPointer l_data_size = new IntPointer(1);
// IntPointer l_tile_index = new IntPointer(1);
// IntPointer l_nb_comps = new IntPointer(1);
// l_nb_comps.put(0);
// IntPointer l_tile_x0 = new IntPointer(1);
// IntPointer l_tile_y0 = new IntPointer(1);
// IntPointer l_tile_x1 = new IntPointer(1);
// IntPointer l_tile_y1 = new IntPointer(1);
//
// while (l_go_on.get()) {
// if (!openjpeg.opj_read_tile_header(codec, l_stream, l_tile_index, l_data_size, l_tile_x0,
// l_tile_y0, l_tile_x1, l_tile_y1, l_nb_comps, l_go_on)) {
// // throw new IOException("Failed to read tile header: " + l_tile_index.get() + "!");
// }
//
// if (l_go_on.get()) {
// if (outBuf == null || l_data_size.get() > outBuf.capacity()) {
// outBuf = ByteBuffer.allocateDirect(l_data_size.get());
// }
//
// if (!openjpeg.opj_decode_tile_data(codec, l_tile_index.get(), outBuf, l_data_size.get(),
// l_stream)) {
// throw new IOException("Failed to decode tile " + l_tile_index.get() + "!");
// }
// LOGGER.debug("tile {} is decoded", l_tile_index.get());
// }
// }
// } else {
long start = System.currentTimeMillis();
/* Get the decoded image */
if (!(openjpeg.opj_decode(codec, lstream, image) && openjpeg.opj_end_decompress(codec, lstream))) {
throw new IOException("Failed to set the decoded image!");
}
LOGGER.debug("OpenJPEG decode time: {} ms", (System.currentTimeMillis() - start)); //$NON-NLS-1$
// }
// if (tile_index >= 0) {
// /* It is just here to illustrate how to use the resolution after set parameters */
// /*
// * if (!openjpeg.opj_set_decoded_resolution_factor(l_codec, 5)) {
// * openjpeg.opj_stream_destroy_v3(l_stream); throw new
// * IOException("Failed to set the resolution factor tile!"); }
// */
//
// if (!openjpeg.opj_get_decoded_tile(codec, l_stream, image, tile_index)) {
// throw new IOException("Failed to decode tile " + tile_index + "!");
// }
// LOGGER.debug("tile {} is decoded", tile_index);
// } else {
//
// /* Get the decoded image */
// if (!(openjpeg.opj_decode(codec, l_stream, image)
// && openjpeg.opj_end_decompress(codec, l_stream))) {
// throw new IOException("Failed to set the decoded image!");
// }
// }
/*
* Has not effect on releasing memory but only keep a reference to be not garbage collected during the
* native decode (ByteBuffer.allocateDirect() has PhantomReference)
*/
buffer.clear();
openjpeg.opj_stream_destroy(lstream);
j2kFile.deallocate();
lstream.deallocate();
lstream = null;
int bands = image.numcomps();
if (bands > 0) {
// if (image.color_space() == openjpeg.OPJ_CLRSPC_SYCC) {
// openjpeg.color_sycc_to_rgb(image);
// }
// if(image.color_space() != openjpeg.OPJ_CLRSPC_SYCC
// && bands == 3 && image->comps[0].dx == image->comps[0].dy
// && image->comps[1].dx != 1 ) {
// image.color_space(openjpeg.OPJ_CLRSPC_SYCC);
// } else if (bands <= 2) {
// image.color_space(openjpeg.OPJ_CLRSPC_GRAY);
// }
// if(image->icc_profile_buf) {
// #if defined(OPJ_HAVE_LIBLCMS1) || defined(OPJ_HAVE_LIBLCMS2)
// color_apply_icc_profile(image); /* FIXME */
// #endif
// free(image->icc_profile_buf);
// image->icc_profile_buf = NULL; image->icc_profile_len = 0;
// }
// Build outputStream here and transform to an array
// Convert band interleaved from openjpeg to pixel interleaved (to display)
if (area == null) {
area = new Rectangle(0, 0, j2kparams.getWidth(), j2kparams.getHeight());
}
int imgSize = area.width * area.height;
int length = imgSize * bands;
Object array = null;
if (bps > 0 && bps <= 16) {
array = bps <= 8 ? new byte[length] : new short[length];
opj_image_comp cp = image.comps().position(0);
int dx = cp.dx();
int dy = cp.dy();
for (int i = 0; i < bands; i++) {
if (i > 0) {
cp = image.comps().position(i);
if (cp.prec() != bps) {
LOGGER.error(
"Cannot read band {} because bits per sample = {}, which is different from the first band = {}.",
new Object[] { i, cp.prec(), bps });
continue;
}
if (cp.dx() != dx || cp.dy() != dy) {
LOGGER.error(
"Cannot read band {} because separation of a sample is different from the first band.",
i);
continue;
}
}
// TODO convert band to pixel interleaved, store in temporary file when size >= 1024
IntPointer intBuf = cp.data();
if (imgSize <= cp.w() * cp.h()) {
if (bps <= 8) {
byte[] data = (byte[]) array;
for (int k = 0; k < imgSize; k++) {
data[k * bands + i] = (byte) intBuf.get(k);
}
} else {
short[] data = (short[]) array;
// boolean signed = params.isSignedData();
// if (signed) {
// int singedOffset = (1 << bps) / 2;
// for (int k = 0; k < imgSize; k++) {
// int val = intBuf.get(k);
// data[k * bands + i] =
// (short) (val < singedOffset ? val + singedOffset : val - singedOffset);
// }
// } else {
for (int k = 0; k < imgSize; k++) {
data[k * bands + i] = (short) intBuf.get(k);
}
// }
}
}
}
}
if (array != null) {
nImage.fillOutputBuffer(array, 0, length);
}
}
} finally {
if (lstream != null) {
openjpeg.opj_stream_destroy(lstream);
lstream.deallocate();
}
if (codec != null) {
openjpeg.opj_destroy_codec(codec);
codec.deallocate();
}
if (image != null) {
openjpeg.opj_image_destroy(image);
image.deallocate();
}
// Do not close inChannel (comes from image input stream)
}
}
return msg;
}
@Override
public String compress(NativeImage nImage, ImageOutputStream ouputStream, ImageWriteParam param) throws IOException {
String msg = null;
if (nImage != null && ouputStream != null && nImage.getInputBuffer() != null) {
try {
J2kParameters params = (J2kParameters) nImage.getImageParameters();
int bps = params.getBitsPerSample();
if (bps < 1 || bps > 16) {
return "OPENJPEG codec: invalid bit per sample: " + bps;
}
int samplesPerPixel = params.getSamplesPerPixel();
if (samplesPerPixel != 1 && samplesPerPixel != 3) {
return "OPENJPEG codec supports only 1 and 3 bands!";
}
Buffer b = nImage.getInputBuffer();
ByteBuffer buffer;
if (b instanceof ByteBuffer) {
buffer = ByteBuffer.allocateDirect(b.limit());
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put((ByteBuffer) b);
} else if (b instanceof ShortBuffer) {
ShortBuffer sBuf = (ShortBuffer) b;
buffer = ByteBuffer.allocateDirect(sBuf.limit() * 2);
buffer.order(ByteOrder.LITTLE_ENDIAN);
while (sBuf.hasRemaining()) {
buffer.putShort(sBuf.get());
}
} else {
return "JPGLS codec exception: not valid input buffer";
}
// set lossless
// parameters.tcp_numlayers = 1;
// parameters.tcp_rates[0] = 0;
// parameters.cp_disto_alloc = 1;
//
// if (!openjpeg.opj_setup_eecoder(codec, parameters)) {
// throw new IOException("Failed to setup the decoder");
// }
} finally {
// Do not close inChannel (comes from image input stream)
}
}
// // set lossless
// parameters.tcp_numlayers = 1;
// parameters.tcp_rates[0] = 0;
// parameters.cp_disto_alloc = 1;
//
// if(djcp->getUseCustomOptions())
// {
// parameters.cblockw_init = djcp->get_cblkwidth();
// parameters.cblockh_init = djcp->get_cblkheight();
// }
//
// // turn on/off MCT depending on transfer syntax
// if(supportedTransferSyntax() == EXS_JPEG2000LosslessOnly)
// parameters.tcp_mct = 0;
// else if(supportedTransferSyntax() == EXS_JPEG2000MulticomponentLosslessOnly)
// parameters.tcp_mct = (image->numcomps >= 3) ? 1 : 0;
//
// // We have no idea how big the compressed pixel data will be and we have no
// // way to find out, so we just allocate a buffer large enough for the raw data
// // plus a little more for JPEG metadata.
// // Yes, this is way too much for just a little JPEG metadata, but some
// // test-images showed that the buffer previously was too small. Plus, at some
// // places charls fails to do proper bounds checking and writes behind the end
// // of the buffer (sometimes way behind its end...).
// size_t size = frameSize + 1024;
// Uint8 *buffer = new Uint8[size];
//
// // Set up the information structure for OpenJPEG
// opj_stream_t *l_stream = NULL;
// opj_codec_t* l_codec = NULL;
// l_codec = opj_create_compress(OPJ_CODEC_J2K);
//
// opj_set_info_handler(l_codec, msg_callback, NULL);
// opj_set_warning_handler(l_codec, msg_callback, NULL);
// opj_set_error_handler(l_codec, msg_callback, NULL);
//
// if (result.good() && !opj_setup_encoder(l_codec, ¶meters, image))
// {
// opj_destroy_codec(l_codec);
// l_codec = NULL;
// result = EC_MemoryExhausted;
// }
//
// DecodeData mysrc((unsigned char*)buffer, size);
// l_stream = opj_stream_create_memory_stream(&mysrc, size, OPJ_FALSE);
//
// if(!opj_start_compress(l_codec,image,l_stream))
// {
// result = EC_CorruptedData;
// }
//
// if(result.good() && !opj_encode(l_codec, l_stream))
// {
// result = EC_InvalidStream;
// }
//
// if(result.good() && opj_end_compress(l_codec, l_stream))
// {
// result = EC_Normal;
// }
//
// opj_stream_destroy(l_stream); l_stream = NULL;
// opj_destroy_codec(l_codec); l_codec = NULL;
// opj_image_destroy(image); image = NULL;
//
// size = mysrc.offset;
return null;
}
@Override
public void dispose() {
}
private Pointer getCodec(int type) {
switch (type) {
case J2K_CFMT: /* JPEG-2000 codestream */
return openjpeg.opj_create_decompress(openjpeg.OPJ_CODEC_J2K);
case JP2_CFMT: /* JPEG 2000 compressed image data */
return openjpeg.opj_create_decompress(openjpeg.OPJ_CODEC_JP2);
default:
return null;
}
}
private static void setParameters(ImageParameters params, opj_image image) {
if (params != null && image != null) {
int bands = image.numcomps();
if (bands > 0) {
opj_image_comp cp = image.comps().position(0);
params.setWidth(cp.w());
params.setHeight(cp.h());
// TODO change this once tile reading has been implemented.
params.setTileWidth(params.getWidth());
params.setTileHeight(params.getHeight());
params.setBitsPerSample(cp.prec());
params.setSamplesPerPixel(bands);
params.setBytesPerLine(
params.getWidth() * params.getSamplesPerPixel() * ((params.getBitsPerSample() + 7) / 8));
params.setSignedData(cp.sgnd() != 0);
// params.setAllowedLossyError(p.allowedlossyerror());
}
}
}
@Override
public NativeImage buildImage(ImageInputStream iis) throws IOException {
int type = getType(iis);
NativeJ2kImage img = new NativeJ2kImage();
J2kParameters params = img.getJ2kParameters();
params.setType(type);
// params.setWidth(sof.getSamplesPerLine());
// params.setHeight(sof.getLines());
// params.setBitsPerSample(sof.getSamplePrecision());
// params.setSamplesPerPixel(sof.getComponents());
// params.setBytesPerLine(params.getWidth() * params.getSamplesPerPixel() * ((params.getBitsPerSample() + 7) /
// 8));
return img;
}
public static int getType(ImageInputStream iis) throws IOException {
iis.mark();
try {
byte[] b = new byte[12];
iis.readFully(b);
// TODO J2K_CFMT (JPIP)
// J2K_CODESTREAM_MAGIC
if ((b[0] & 0xFF) == 0xFF && (b[1] & 0xFF) == 0x4F && (b[2] & 0xFF) == 0xFF && (b[3] & 0xFF) == 0x51) {
return J2K_CFMT;
}
// JP2_MAGIC
if ((b[0] & 0xFF) == 0x0D && (b[1] & 0xFF) == 0x0A && (b[2] & 0xFF) == 0x87 && (b[3] & 0xFF) == 0x0A) {
return JP2_CFMT;
}
// JP2_RFC3745_MAGIC
if (b[0] == 0 && b[1] == 0 && b[2] == 0 && (b[3] & 0xFF) == 0x0C && (b[4] & 0xff) == 0x6A
&& (b[5] & 0xFF) == 0x50 && (b[6] & 0xFF) == 0x20 && (b[7] & 0xFF) == 0x20 && (b[8] & 0xFF) == 0x0D
&& (b[9] & 0xFF) == 0x0A && (b[10] & 0xFF) == 0x87 && (b[11] & 0xFF) == 0x0A) {
return JP2_CFMT;
}
return -1;
} finally {
iis.reset();
}
}
}