/*******************************************************************************
* 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.jpeg.internal;
import java.io.EOFException;
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.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.jpeg.JpegParameters;
import org.weasis.jpeg.NativeJPEGImage;
import org.weasis.jpeg.cpp.libijg;
import org.weasis.jpeg.cpp.libijg.DJDecompressIJG12Bit;
import org.weasis.jpeg.cpp.libijg.DJDecompressIJG16Bit;
import org.weasis.jpeg.cpp.libijg.DJDecompressIJG8Bit;
import org.weasis.jpeg.cpp.libijg.RETURN_MSG;
import org.weasis.jpeg.cpp.libijg.jpeg_decompress_struct;
public class JpegCodec implements NativeCodec {
private static final Logger LOGGER = LoggerFactory.getLogger(JpegCodec.class);
public JpegCodec() {
}
@Override
public String readHeader(NativeImage nImage) throws IOException {
String msg = null;
StreamSegment seg = nImage.getStreamSegment();
if (seg != null) {
try (DecoderIJG decomp = new DJDecompressIJG8Bit()) {
ByteBuffer buffer = seg.getDirectByteBuffer(0);
decomp.init(false);
RETURN_MSG val = decomp.readHeader(buffer, buffer.limit(), false);
if (val != null && val.code() == libijg.OK) {
setParameters(nImage.getImageParameters(), decomp);
} else if (val != null) {
msg = val.msg().getString();
}
// keep a reference to be not garbage collected
buffer.clear();
}
}
return msg;
}
@Override
public String decompress(NativeImage nImage, ImageReadParam param) throws IOException {
// TODO use ImageReadParam
String msg = null;
StreamSegment seg = nImage.getStreamSegment();
if (seg != null) {
ImageParameters params = nImage.getImageParameters();
int bps = params.getBitsPerSample();
if (bps < 1 || bps > 16) {
return "JPEG codec: invalid bit per sample: " + bps;
}
try (DecoderIJG decomp = bps > 12 ? new DJDecompressIJG16Bit()
: bps > 8 ? new DJDecompressIJG12Bit() : new DJDecompressIJG8Bit()) {
int segmentFragment = 0;
ByteBuffer buffer = seg.getDirectByteBuffer(segmentFragment);
boolean signed = params.isSignedData();
// Force to convert YBR to RGB even when jpeg header has an RGB input color model. Not supported for
// signed data.
decomp.init(false);
// Force RBG (for gray keeps grayscale model), except for signed data where the conversion is not
// supported.
RETURN_MSG val = decomp.readHeader(buffer, buffer.limit(), signed);
if (val != null && val.code() == libijg.OK) {
setParameters(nImage.getImageParameters(), decomp);
LOGGER.debug("Input color space {}", decomp.getJpeg_DecompressStruct().jpeg_color_space());
// Build outputStream here and transform to an array
ByteBuffer outBuf = ByteBuffer.allocateDirect(params.getBytesPerLine() * params.getHeight());
outBuf.order(ByteOrder.LITTLE_ENDIAN);
int result = libijg.EJ_Suspension;
while (result == libijg.EJ_Suspension) {
val = decomp.decode(buffer, buffer.limit(), outBuf, outBuf.limit());
result = val.code();
if (result == libijg.EJ_Suspension) {
segmentFragment++;
buffer = seg.getDirectByteBuffer(segmentFragment);
}
}
bps = params.getBitsPerSample();
nImage.setOutputBuffer((bps > 8 && bps <= 16) ? outBuf.asShortBuffer() : outBuf);
} else if (val != null) {
msg = val.msg().getString();
}
// keep a reference to be not garbage collected
buffer.clear();
}
}
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 {
JpegParameters params = (JpegParameters) nImage.getImageParameters();
int bps = params.getBitsPerSample();
if (bps < 1 || bps > 16) {
return "JPEG codec: invalid bit per sample: " + bps;
}
int samplesPerPixel = params.getSamplesPerPixel();
if (samplesPerPixel != 1 && samplesPerPixel != 3) {
return "JPEG codec supports only 1 and 3 bands!";
}
// DecoderIJG decomp =
// bps > 12 ? new DJDecompressIJG16Bit() : bps > 8 ? new DJDecompressIJG12Bit()
// : new DJDecompressIJG8Bit();
// DJCodecParameter djParams = new DJCodecParameter(libijg.ECC_lossyYCbCr);
// EncoderIJG comp = new DJCompressIJG8Bit(djParams, libijg.EJM_baseline, (byte) 90);
long start = System.currentTimeMillis();
// TODO get directly array
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 "JPEG driver exception: not valid input buffer";
}
buffer.flip();
long stop = System.currentTimeMillis();
System.out.println("Convert array time: " + (stop - start) + " ms"); //$NON-NLS-1$
// Build outputStream here and transform to an array
ByteBuffer outBuf = ByteBuffer.allocateDirect(params.getWidth() * params.getHeight()
* params.getSamplesPerPixel() * params.getBitsPerSample() / 16);
outBuf.order(params.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
int columns = params.getWidth();
int rows = params.getHeight();
int interpr = samplesPerPixel == 1 ? libijg.EPI_Monochrome2 : libijg.EPI_RGB;
IntPointer bytesWritten = new IntPointer(1);
// start = System.currentTimeMillis();
// // byte[] to = null;
// RETURN_MSG val = comp.encode(columns, rows, interpr, samplesPerPixel, buffer, outBuf, bytesWritten);
// // keep a reference to be not garbage collected
// buffer.clear();
// if (val == null || val.code() != libijg.OK) {
// msg = val == null ? "error" : val.msg().getString();
// }
// stop = System.currentTimeMillis();
// System.out
// .println("Native encoder time: " + (stop - start) + " ms. BytesWritten add: " + bytesWritten.get());
// //$NON-NLS-1$
// if (msg == null) {
// // ByteBuffer outBuf = ByteBuffer.wrap(to);
// outBuf.rewind();
// // TODO write directly in native lib by calling: ouputStream.write(outBuf.array(), 0, 4096);
// NativeImage.writeByteBuffer(ouputStream, outBuf, 154618);
// System.out.println("Write encoder time: " + (System.currentTimeMillis() - stop) + " ms");
// //$NON-NLS-1$
// }
} finally {
nImage.setInputBuffer(null);
// Do not close inChannel (comes from image input stream)
}
}
return msg;
}
@Override
public void dispose() {
}
private static void setParameters(ImageParameters params, DecoderIJG codec) {
if (params != null && codec != null) {
jpeg_decompress_struct p = codec.getJpeg_DecompressStruct();
params.setWidth(p.image_width());
params.setHeight(p.image_height());
/*
* Adjust tile size to image size. RenderedImage will handle only one tile as this decoder doesn't support
* well region reading (slow).
*/
params.setTileWidth(params.getWidth());
params.setTileHeight(params.getHeight());
params.setBitsPerSample(p.data_precision());
params.setSamplesPerPixel(p.num_components());
params.setBytesPerLine(
params.getWidth() * params.getSamplesPerPixel() * ((params.getBitsPerSample() + 7) / 8));
// params.setAllowedLossyError(p.allowedlossyerror());
}
}
@Override
public NativeImage buildImage(ImageInputStream iis) throws IOException {
SOFSegment sof = getSOFSegment(iis);
NativeJPEGImage img = new NativeJPEGImage();
if (sof != null) {
ImageParameters params = img.getJpegParameters();
params.setWidth(sof.getSamplesPerLine());
params.setHeight(sof.getLines());
// Adjust tile size to image size for writer
params.setTileWidth(params.getWidth());
params.setTileHeight(params.getHeight());
params.setBitsPerSample(sof.getSamplePrecision());
params.setSamplesPerPixel(sof.getComponents());
params.setBytesPerLine(
params.getWidth() * params.getSamplesPerPixel() * ((params.getBitsPerSample() + 7) / 8));
}
return img;
}
public static SOFSegment getSOFSegment(ImageInputStream iis) throws IOException {
iis.mark();
try {
int byte1 = iis.read();
int byte2 = iis.read();
// Magic numbers for JPEG (general jpeg marker)
if ((byte1 != 0xFF) || (byte2 != 0xD8)) {
return null;
}
do {
byte1 = iis.read();
byte2 = iis.read();
// Something wrong, but try to read it anyway
if (byte1 != 0xFF) {
break;
}
// Start of scan
if (byte2 == 0xDA) {
break;
}
// Start of Frame, also known as SOF55, indicates a JPEG-LS file.
if (byte2 == 0xF7) {
return getSOF(iis, (byte1 << 8) + byte2);
}
// 0xffc0: // SOF_0: JPEG baseline
// 0xffc1: // SOF_1: JPEG extended sequential DCT
// 0xffc2: // SOF_2: JPEG progressive DCT
// 0xffc3: // SOF_3: JPEG lossless sequential
if ((byte2 >= 0xC0) && (byte2 <= 0xC3)) {
return getSOF(iis, (byte1 << 8) + byte2);
}
// 0xffc5: // SOF_5: differential (hierarchical) extended sequential, Huffman
// 0xffc6: // SOF_6: differential (hierarchical) progressive, Huffman
// 0xffc7: // SOF_7: differential (hierarchical) lossless, Huffman
if ((byte2 >= 0xC5) && (byte2 <= 0xC7)) {
return getSOF(iis, (byte1 << 8) + byte2);
}
// 0xffc9: // SOF_9: extended sequential, arithmetic
// 0xffca: // SOF_10: progressive, arithmetic
// 0xffcb: // SOF_11: lossless, arithmetic
if ((byte2 >= 0xC9) && (byte2 <= 0xCB)) {
return getSOF(iis, (byte1 << 8) + byte2);
}
// 0xffcd: // SOF_13: differential (hierarchical) extended sequential, arithmetic
// 0xffce: // SOF_14: differential (hierarchical) progressive, arithmetic
// 0xffcf: // SOF_15: differential (hierarchical) lossless, arithmetic
if ((byte2 >= 0xCD) && (byte2 <= 0xCF)) {
return getSOF(iis, (byte1 << 8) + byte2);
}
int length = iis.read() << 8;
length += iis.read();
length -= 2;
while (length > 0) {
length -= iis.skipBytes(length);
}
} while (true);
return null;
} finally {
iis.reset();
}
}
protected static SOFSegment getSOF(ImageInputStream iis, int marker) throws IOException {
readUnsignedShort(iis);
int samplePrecision = readUnsignedByte(iis);
int lines = readUnsignedShort(iis);
int samplesPerLine = readUnsignedShort(iis);
int componentsInFrame = readUnsignedByte(iis);
return new SOFSegment(marker, samplePrecision, lines, samplesPerLine, componentsInFrame);
}
private static final int readUnsignedByte(ImageInputStream iis) throws IOException {
int ch = iis.read();
if (ch < 0) {
throw new EOFException();
}
return ch;
}
private static final int readUnsignedShort(ImageInputStream iis) throws IOException {
int ch1 = iis.read();
int ch2 = iis.read();
if ((ch1 | ch2) < 0) {
throw new EOFException();
}
return (ch1 << 8) + ch2;
}
}