/* * Copyright (c) 2002-2007, Communications and Remote Sensing Laboratory, Universite catholique de Louvain (UCL), Belgium * Copyright (c) 2002-2007, Professor Benoit Macq * Copyright (c) 2002-2007, Patrick Piscaglia, Telemis s.a. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package org.openJpeg; import java.io.File; import java.util.Vector; /** This class encodes one image into the J2K format, * using the OpenJPEG.org library. * To be able to log messages, the called must register a IJavaJ2KEncoderLogger object. */ public class OpenJPEGJavaEncoder { public interface IJavaJ2KEncoderLogger { public void logEncoderMessage(String message); public void logEncoderError(String message); } private static boolean isInitialized = false; // ===== Compression parameters =============> // These value may be changed for each image private String[] encoder_arguments = null; /** number of resolutions decompositions */ private int nbResolutions = -1; /** the quality layers, expressed as compression rate */ private float[] ratioLayers = null; /** the quality layers, expressed as PSNR values. This variable, if defined, has priority over the ratioLayers variable */ private float[] psnrLayers = null; /** Contains the 8 bpp version of the image. May NOT be filled together with image16 or image24.<P> * We store the 8 or 16 bpp version of the original image while the encoder uses a 32 bpp version, because <UL> * <LI> the storage capacity required is smaller * <LI> the transfer Java --> C will be faster * <LI> the conversion byte/short ==> int will be done faster by the C * </UL>*/ private byte[] image8 = null; /** Contains the 16 bpp version of the image. May NOT be filled together with image8 or image24*/ private short[] image16 = null; /** Contains the 24 bpp version of the image. May NOT be filled together with image8 or image16 */ private int[] image24 = null; /** Holds the result of the compression, i.e. the J2K compressed bytecode */ private byte compressedStream[] = null; /** Holds the compressed stream length, which may be smaller than compressedStream.length if this byte[] is pre-allocated */ private long compressedStreamLength = -1; /** Holds the compressed version of the index file, returned by the encoder */ private byte compressedIndex[] = null; /** Width and Height of the image */ private int width = -1; private int height = -1; private int depth = -1; /** Tile size. We suppose the same size for the horizontal and vertical tiles. * If size == -1 ==> no tiling */ private int tileSize = -1; // <===== Compression parameters ============= private Vector<IJavaJ2KEncoderLogger> loggers = new Vector(); public OpenJPEGJavaEncoder(String openJPEGlibraryFullPathAndName, IJavaJ2KEncoderLogger messagesAndErrorsLogger) throws ExceptionInInitializerError { this(openJPEGlibraryFullPathAndName); loggers.addElement(messagesAndErrorsLogger); } public OpenJPEGJavaEncoder(String openJPEGlibraryFullPathAndName) throws ExceptionInInitializerError { if (!isInitialized) { try { String absolutePath = (new File(openJPEGlibraryFullPathAndName)).getCanonicalPath(); System.load(absolutePath); isInitialized = true; } catch (Throwable t) { t.printStackTrace(); throw new ExceptionInInitializerError("OpenJPEG Java Encoder: probably impossible to find the C library"); } } } public void addLogger(IJavaJ2KEncoderLogger messagesAndErrorsLogger) { loggers.addElement(messagesAndErrorsLogger); } public void removeLogger(IJavaJ2KEncoderLogger messagesAndErrorsLogger) { loggers.removeElement(messagesAndErrorsLogger); } /** This method compresses the given image.<P> * It returns the compressed J2K codestream into the compressedStream byte[].<P> * It also returns the compression index as a compressed form, into the compressedIndex byte[].<P> * One of the image8, image16 or image24 arrays must be correctly initialized and filled.<P> * The width, height and depth variables must be correctly filled.<P> * The nbResolutions, nbLayers and if needed the float[] psnrLayers or ratioLayers must also be filled before calling this method. */ public void encodeImageToJ2K() { // Need to allocate / reallocate the compressed stream buffer ? (size = max possible size = original image size) if (compressedStream== null || (compressedStream.length != width*height*depth/8)) { logMessage("OpenJPEGJavaEncoder.encodeImageToJ2K: (re-)allocating " + (width*height*depth/8) + " bytes for the compressedStream"); compressedStream = new byte[width*height*depth/8]; } // Arguments = // - number of resolutions "-n 5" : 2 // - size of tile "-t 512,512" : 2 // // Image width, height, depth and pixels are directly fetched by C from the Java class int nbArgs = 2 + (tileSize == -1 ? 0 : 2) + (encoder_arguments != null ? encoder_arguments.length : 0); if (psnrLayers != null && psnrLayers.length>0 && psnrLayers[0] != 0) // If psnrLayers is defined and doesn't just express "lossless" nbArgs += 2; else if (ratioLayers != null && ratioLayers.length>0 && ratioLayers[0]!=0.0) nbArgs += 2; String[] arguments = new String[nbArgs]; int offset = 0; arguments[offset] = "-n"; arguments[offset+1] = "" + nbResolutions; offset += 2; if (tileSize!= -1) { arguments[offset++] = "-t"; arguments[offset++] = "" + tileSize + "," + tileSize; } // If PSNR layers are defined, use them to encode the images if (psnrLayers != null && psnrLayers.length>0 && psnrLayers[0]!=-1) { arguments[offset++] = "-q"; String s = ""; for (int i=0; i<psnrLayers.length; i++) s += psnrLayers[i] + ","; arguments[offset++] = s.substring(0, s.length()-1); } else if (ratioLayers != null && ratioLayers.length>0 && ratioLayers[0]!=0.0) { // Specify quality ratioLayers, as compression ratios arguments[offset++] = "-r"; String s = ""; for (int i=0; i<ratioLayers.length; i++) s += ratioLayers[i] + ","; arguments[offset++] = s.substring(0, s.length()-1); } if (encoder_arguments != null) { for (int i=0; i<encoder_arguments.length; i++) { arguments[i+offset] = encoder_arguments[i]; } } logMessage("Encoder additional arguments = " + arrayToString(arguments)); long startTime = (new java.util.Date()).getTime(); compressedStreamLength = internalEncodeImageToJ2K(arguments); logMessage("compression time = " + ((new java.util.Date()).getTime() - startTime) + " msec"); } /** * Fills the compressedStream byte[] and the compressedIndex byte[] * @return the codestream length. */ private native long internalEncodeImageToJ2K(String[] parameters); /** Image depth in bpp */ public int getDepth() { return depth; } /** Image depth in bpp */ public void setDepth(int depth) { this.depth = depth; } /** Image height in pixels */ public int getHeight() { return height; } /** Image height in pixels */ public void setHeight(int height) { this.height = height; } /** This method must be called in depth in [9,16]. * @param an array of shorts, containing width*height values */ public void setImage16(short[] image16) { this.image16 = image16; } /** This method must be called in depth in [17,24] for RGB images. * @param an array of int, containing width*height values */ public void setImage24(int[] image24) { this.image24 = image24; } /** This method must be called in depth in [1,8]. * @param an array of bytes, containing width*height values */ public void setImage8(byte[] image8) { this.image8 = image8; } /** Return the ratioLayers, i.e. the compression ratio for each quality layer. * If the last value is 0.0, last layer is lossless compressed. */ public float[] getRatioLayers() { return ratioLayers; } /** * sets the quality layers. * At least one level. * Each level is expressed as a compression ratio (float). * If the last value is 0.0, the last layer will be losslessly compressed */ public void setRatioLayers(float[] layers) { this.ratioLayers = layers; } /** Return the PSNR Layers, i.e. the target PSNR for each quality layer. * If the last value is -1, last layer is lossless compressed. */ public float[] getPsnrLayers() { return psnrLayers; } /** * sets the quality layers. * At least one level. * Each level is expressed as a target PSNR (float). * If the last value is -1, the last layer will be losslessly compressed */ public void setPsnrLayers(float[] layers) { this.psnrLayers = layers; } /** Set the number of resolutions that must be created */ public void setNbResolutions(int nbResolutions) { this.nbResolutions = nbResolutions; } public int getWidth() { return width; } /** Width of the image, in pixels */ public void setWidth(int width) { this.width = width; } /** Return the compressed index file. * Syntax: TODO PP: */ public byte[] getCompressedIndex() { return compressedIndex; } public void setCompressedIndex(byte[] index) { compressedIndex = index; } public byte[] getCompressedStream() { return compressedStream; } public void reset() { nbResolutions = -1; ratioLayers = null; psnrLayers = null; image8 = null; image16 = null; image24 = null; compressedStream = null; compressedIndex = null; width = -1; height = -1; depth = -1; } public short[] getImage16() { return image16; } public int[] getImage24() { return image24; } public byte[] getImage8() { return image8; } /** Sets the size of the tiles. We assume square tiles */ public void setTileSize(int tileSize) { this.tileSize = tileSize; } /** Contains all the encoding arguments other than the input/output file, compression ratio, tile size */ public void setEncoderArguments(String[] argumentsForTheEncoder) { encoder_arguments = argumentsForTheEncoder; } public void logMessage(String message) { for (IJavaJ2KEncoderLogger logger:loggers) logger.logEncoderMessage(message); } public void logError(String error) { for (IJavaJ2KEncoderLogger logger:loggers) logger.logEncoderError(error); } public long getCompressedStreamLength() { return compressedStreamLength; } private String arrayToString(String[] array) { if (array == null) return "NULL"; StringBuffer sb = new StringBuffer(); for (int i=0; i<array.length; i++) sb.append(array[i]).append(" "); sb.delete(sb.length()-1, sb.length()); return sb.toString(); } }