/* * ImageI/O-Ext - OpenSource Java Image translation Library * http://www.geo-solutions.it/ * http://java.net/projects/imageio-ext/ * (C) 2007 - 2009, GeoSolutions * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * either version 3 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package it.geosolutions.imageio.plugins.jp2k; import it.geosolutions.imageio.plugins.jp2k.box.BitsPerComponentBox; import it.geosolutions.imageio.plugins.jp2k.box.BoxUtilities; import it.geosolutions.imageio.plugins.jp2k.box.ChannelDefinitionBox; import it.geosolutions.imageio.plugins.jp2k.box.ColorSpecificationBox; import it.geosolutions.imageio.plugins.jp2k.box.ComponentMappingBox; import it.geosolutions.imageio.plugins.jp2k.box.ImageHeaderBox; import it.geosolutions.imageio.plugins.jp2k.box.PaletteBox; import it.geosolutions.imageio.stream.input.FileImageInputStreamExt; import it.geosolutions.imageio.utilities.Utilities; import it.geosolutions.util.KakaduUtilities; import java.awt.Rectangle; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; import java.awt.image.DataBufferUShort; import java.awt.image.IndexColorModel; import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.PixelInterleavedSampleModel; import java.awt.image.Raster; import java.awt.image.RasterFormatException; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeModel; import kdu_jni.Jp2_family_src; import kdu_jni.Jpx_codestream_source; import kdu_jni.Jpx_input_box; import kdu_jni.Jpx_source; import kdu_jni.KduException; import kdu_jni.Kdu_codestream; import kdu_jni.Kdu_coords; import kdu_jni.Kdu_dims; import kdu_jni.Kdu_global; import kdu_jni.Kdu_simple_file_source; import kdu_jni.Kdu_stripe_decompressor; import com.sun.media.imageioimpl.common.ImageUtil; /** * <code>JP2KakaduImageReader</code> is a <code>ImageReader</code> able to * create {@link RenderedImage} from JP2 files, leveraging on Kdu_jni bindings. * * @author Simone Giannecchini, GeoSolutions. * @author Daniele Romagnoli, GeoSolutions. */ public class JP2KKakaduImageReader extends ImageReader { // private boolean initializedJp2Boxes = false; private static Logger LOGGER = Logger .getLogger("it.geosolutions.imageio.plugins.jp2k"); static { final String level = System.getProperty("it.geosolutions.loggerlevel"); if (level != null && level.equalsIgnoreCase("FINE")) { LOGGER.setLevel(Level.FINE); } } /** Size of the Temp Buffer, used when reading from an image input stream */ private final static int TEMP_BUFFER_SIZE = 64 * 1024; private boolean deleteInputFile; /** The dataset input source */ private File inputFile = null; /** The data input source name */ private String fileName = null; private boolean isRawSource; /** A fileWalker to scan boxes */ private JP2KFileWalker fileWalker; private final List<JP2KCodestreamProperties> multipleCodestreams = new ArrayList<JP2KCodestreamProperties>(); private int numImages = 1; protected JP2KKakaduImageReader(ImageReaderSpi originatingProvider) { super(originatingProvider); KakaduUtilities.initializeKakaduMessagesManagement(); } /** * Checks if the specified ImageIndex is valid. * * @param imageIndex * the specified imageIndex * * @throws IndexOutOfBoundsException * if imageIndex is lower than 0 or if is greater than the * max number (-1) of images available within the data * source contained within the source */ protected void checkImageIndex(final int imageIndex) { if (imageIndex < 0 || imageIndex > numImages) { final StringBuffer sb = new StringBuffer( "Illegal imageIndex specified = ").append(imageIndex) .append(", while the valid imageIndex"); if (numImages > 1) // There are N Images. sb.append(" range should be [0,").append(numImages - 1).append("]!"); else // Only the imageIndex 0 is valid. sb.append(" should be 0!"); throw new IndexOutOfBoundsException(sb.toString()); } } /** * Returns the height in pixel of the image * * @param the * index of the selected image */ public int getHeight(int imageIndex) throws IOException { checkImageIndex(imageIndex); return multipleCodestreams.get(imageIndex).getHeight(); } /** * Returns the width in pixel of the image * * @param the * index of the selected image */ public int getWidth(int imageIndex) throws IOException { checkImageIndex(imageIndex); return multipleCodestreams.get(imageIndex).getWidth(); } /** * Returns an <code>IIOMetadata</code> object containing metadata * associated with the given image. * * @param imageIndex the index of the image whose metadata is to * be retrieved. * * @return an <code>IIOMetadata</code> object. * @see javax.imageio.ImageReader#getImageMetadata(int) */ public IIOMetadata getImageMetadata(int imageIndex) throws IOException { return new JP2KImageMetadata(multipleCodestreams.get(imageIndex)); } /** * Returns an <code>IIOMetadata</code> object representing the * metadata associated with the input source as a whole. * * @return an <code>IIOMetadata</code> object. * @see javax.imageio.ImageReader#getStreamMetadata() */ public IIOMetadata getStreamMetadata() throws IOException { if (isRawSource) throw new UnsupportedOperationException( "Raw source detected. Actually, unable to get stream metadata"); return new JP2KStreamMetadata(fileWalker.getJP2KBoxesTree(), numImages); } /** * Returns an <code>Iterator</code> containing possible image types to * which the given image may be decoded, in the form of * <code>ImageTypeSpecifiers</code>s. At least one legal image type will * be returned. This implementation simply returns an * <code>ImageTypeSpecifier</code> set in compliance with the property of * the dataset contained within the underlying data source. * * @param imageIndex * the index of the image to be retrieved. * * @return an <code>Iterator</code> containing possible image types to * which the given image may be decoded, in the form of * <code>ImageTypeSpecifiers</code>s * @see javax.imageio.ImageReader#getImageTypes(int) */ public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException { checkImageIndex(imageIndex); final List<ImageTypeSpecifier> l = new java.util.ArrayList<ImageTypeSpecifier>(); final JP2KCodestreamProperties codestreamP = multipleCodestreams.get(imageIndex); // Setting SampleModel and ColorModel for the whole image if (codestreamP.getColorModel() == null || codestreamP.getSampleModel() == null) { try { initializeSampleModelAndColorModel(codestreamP); } catch (KduException kdue) { throw new RuntimeException( "Error while setting sample and color model", kdue); } } final ImageTypeSpecifier imageType = new ImageTypeSpecifier(codestreamP.getColorModel(), codestreamP.getSampleModel()); l.add(imageType); return l.iterator(); } /** * Returns the number of images contained in the source. */ public int getNumImages(boolean allowSearch) throws IOException { return numImages; } /** * Read the image and returns it as a complete <code>BufferedImage</code>, * using a supplied <code>ImageReadParam</code>. * * @param imageIndex * the index of the desired image. */ public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { checkImageIndex(imageIndex); // /////////////////////////////////////////////////////////// // // STEP 0. // ------- // Setting local variables for i-th codestream // // /////////////////////////////////////////////////////////// JP2KCodestreamProperties codestreamP = multipleCodestreams.get(imageIndex); final int maxAvailableQualityLayers = codestreamP.getMaxAvailableQualityLayers(); final int[] componentIndexes = codestreamP.getComponentIndexes(); final int nComponents = codestreamP.getNumComponents(); final int maxBitDepth = codestreamP.getMaxBitDepth(); final int height = codestreamP.getHeight(); final int width = codestreamP.getWidth(); final ColorModel cm = codestreamP.getColorModel(); final SampleModel sm = codestreamP.getSampleModel(); Kdu_simple_file_source localRawSource = null; Jp2_family_src localFamilySource = null; Jpx_source localWrappedSource = null; // get a default set of ImageReadParam if needed. if (param == null) param = getDefaultReadParam(); // /////////////////////////////////////////////////////////// // // STEP 1. // ------- // local variables initialization // // /////////////////////////////////////////////////////////// // The destination image properties final Rectangle destinationRegion = new Rectangle(0, 0, -1, -1); // The source region image properties final Rectangle sourceRegion = new Rectangle(0, 0, -1, -1); // The properties of the image we need to load from Kakadu final Rectangle requiredRegion = new Rectangle(0, 0, -1, -1); // Subsampling Factors int xSubsamplingFactor = -1; int ySubsamplingFactor = -1; // boolean used to specify when resampling is required (as an instance, // different subsampling factors (xSS != ySS) require resampling) boolean resamplingIsRequired = false; // /////////////////////////////////////////////////////////// // // STEP 2. // ------- // parameters management (retrieve user defined readParam and // further initializations) // // /////////////////////////////////////////////////////////// if (!(param instanceof JP2KKakaduImageReadParam)) { // The parameter is not of JP2KakaduImageReadParam type but // simply an ImageReadParam instance (the superclass) // we need to build a proper JP2KakaduImageReadParam prior // to start parameters parsing. JP2KKakaduImageReadParam jp2kParam = (JP2KKakaduImageReadParam) getDefaultReadParam(); jp2kParam.initialize(param); param = jp2kParam; } // selected interpolation type final int interpolationType = ((JP2KKakaduImageReadParam) param).getInterpolationType(); // specified quality layers int qualityLayers = ((JP2KKakaduImageReadParam) param).getQualityLayers(); if (qualityLayers != -1) { // qualityLayers != -1 means that the user have specified that value if (qualityLayers > maxAvailableQualityLayers) // correct the user defined qualityLayers parameter if this // exceed the max number of available quality layers qualityLayers = maxAvailableQualityLayers; } else qualityLayers = 0; // SubSampling variables initialization xSubsamplingFactor = param.getSourceXSubsampling(); ySubsamplingFactor = param.getSourceYSubsampling(); // // // Retrieving Information about Source Region and doing additional // initialization operations. // // computeRegions(width, height, param, sourceRegion, destinationRegion); // //////////////////////////////////////////////////////////////////// // // STEP 3. // ------- // check if the image need to be resampled/rescaled and find the proper // scale factor as well as the size of the required region we need to // provide to kakadu. // // //////////////////////////////////////////////////////////////////// final int[] resolutionInfo = new int[2]; resamplingIsRequired = getRequiredRegionsAndResolutions(codestreamP, xSubsamplingFactor, ySubsamplingFactor, sourceRegion, destinationRegion, requiredRegion, resolutionInfo); final int nDiscardLevels = resolutionInfo[1]; // Setting the destination Buffer Size which will contains the samples // coming from stripe_decompressor BufferedImage bi = null; try { DataBuffer imageBuffer = null; // //////////////////////////////////////////////////////////////// // // STEP 4. // ------- // Initialize sources and codestream // //////////////////////////////////////////////////////////////// // Opening data source Kdu_codestream codestream = new Kdu_codestream(); if (!isRawSource) { localFamilySource = new Jp2_family_src(); localWrappedSource = new Jpx_source(); localFamilySource.Open(fileName); localWrappedSource.Open(localFamilySource, true); final Jpx_codestream_source stream = localWrappedSource.Access_codestream(imageIndex); final Jpx_input_box inputbox = stream.Open_stream(); codestream.Create(inputbox); } else { localRawSource = new Kdu_simple_file_source(fileName); codestream.Create(localRawSource); } // //////////////////////////////////////////////////////////////// // // STEP 5. // ------- // Set parameters for stripe decompression // //////////////////////////////////////////////////////////////// final Kdu_dims dims = new Kdu_dims(); codestream.Apply_input_restrictions(0, nComponents, nDiscardLevels, qualityLayers, null, Kdu_global.KDU_WANT_OUTPUT_COMPONENTS); if (LOGGER.isLoggable(Level.FINE)){ final Kdu_dims original_dims = new Kdu_dims(); codestream.Get_dims(-1, original_dims); StringBuilder sb = new StringBuilder("Original Hi-Res Image Region is: x=").append(original_dims.Access_pos().Get_x()) .append(" y=").append(original_dims.Access_pos().Get_y()) .append(" w=").append(original_dims.Access_size().Get_x()).append(" h=") .append(original_dims.Access_size().Get_y()); LOGGER.fine(sb.toString()); } final Kdu_dims dimsROI = new Kdu_dims(); codestream.Get_dims(0, dims); if (LOGGER.isLoggable(Level.FINE)){ final int mappedX = dims.Access_pos().Get_x(); final int mappedY = dims.Access_pos().Get_y(); final int mappedWidth = dims.Access_size().Get_x(); final int mappedHeight = dims.Access_size().Get_y(); StringBuilder sb = new StringBuilder("Mapped Region is: x=").append(mappedX).append(" y=").append(mappedY) .append(" w=").append(mappedWidth).append(" h=").append(mappedHeight); LOGGER.fine(sb.toString()); } // Checks on the required region and mapped region, in compliance with // Formula 11.1 of Taubman's JPEG2000 compression book. checkBounds(dims, requiredRegion); final int destBufferSize = (requiredRegion.height * requiredRegion.width) * nComponents; setDimsForCrop(dims,requiredRegion); // Getting a region of interest. codestream.Map_region(0, dims, dimsROI); if (LOGGER.isLoggable(Level.FINE)){ final int mappedROIWidth = dimsROI.Access_size().Get_x(); final int mappedROIHeight = dimsROI.Access_size().Get_y(); final int mappedROIX= dimsROI.Access_pos().Get_x(); final int mappedROIY = dimsROI.Access_pos().Get_y(); StringBuilder sb = new StringBuilder("ROI Region is: x=").append(mappedROIX).append(" y=").append(mappedROIY) .append(" w=").append(mappedROIWidth).append(" h=").append(mappedROIHeight); LOGGER.fine(sb.toString()); } codestream.Apply_input_restrictions(nComponents, componentIndexes, nDiscardLevels, qualityLayers, dimsROI, Kdu_global.KDU_WANT_OUTPUT_COMPONENTS); // // // // Setting parameters for stripe decompression // // // // Array with one entry for each image component, identifying the // number of lines to be decompressed for that component in the // present call. All entries must be non-negative. final int[] stripeHeights = new int[nComponents]; // Array with one entry for each image component, identifying // the separation between horizontally adjacent samples within the // corresponding stripe buffer found in the stripe_bufs array. final int[] sampleGap = new int[nComponents]; // Array with one entry for each image component, identifying // the separation between vertically adjacent samples within the // corresponding stripe buffer found in the stripe_bufs array. final int[] rowGap = new int[nComponents]; // Array with one entry for each image component, identifying the // position of the first sample of that component within the buffer // array. final int[] sampleOffset = new int[nComponents]; // Array with one entry for each image component, identifying the // number of significant bits used to represent each sample. // There is no implied connection between the precision values, P, // and the bit-depth, B, of each image component, as found in the // code-stream's SIZ marker segment, and returned via // kdu_codestream::get_bit_depth. The original image sample // bit-depth, B, may be larger or smaller than the value of P // supplied via the precisions argument. The samples returned by // pull_stripe all have a nominally signed representation unless // otherwise indicated by a non-NULL isSigned argument final int precision[] = new int[nComponents]; for (int component = 0; component < nComponents; component++) { stripeHeights[component] = requiredRegion.height; sampleGap[component] = nComponents; rowGap[component] = requiredRegion.width * nComponents; sampleOffset[component] = component; precision[component] = codestream.Get_bit_depth(component); } // //////////////////////////////////////////////////////////////// // // STEP 6. // ------- // Stripe decompressor data loading // //////////////////////////////////////////////////////////////// Kdu_stripe_decompressor decompressor = new Kdu_stripe_decompressor(); decompressor.Start(codestream); /** * @todo: actually, we only handle fixed point types. No float or * double data are handled. */ if (maxBitDepth <= 8) { final byte[] bufferValues = new byte[destBufferSize]; decompressor.Pull_stripe(bufferValues, stripeHeights, sampleOffset, sampleGap, rowGap, precision); imageBuffer = new DataBufferByte(bufferValues, destBufferSize); } else if (maxBitDepth > 8 && maxBitDepth <= 16) { final boolean[] isSigned = new boolean[nComponents]; for (int i = 0; i < isSigned.length; i++) isSigned[i] = codestream.Get_signed(i); final short[] bufferValues = new short[destBufferSize]; decompressor.Pull_stripe(bufferValues, stripeHeights, sampleOffset, sampleGap, rowGap, precision, isSigned); imageBuffer = new DataBufferUShort(bufferValues, destBufferSize); } else if (maxBitDepth > 16 && maxBitDepth <= 32) { final int[] bufferValues = new int[destBufferSize]; decompressor.Pull_stripe(bufferValues, stripeHeights, sampleOffset, sampleGap, rowGap, precision); imageBuffer = new DataBufferInt(bufferValues, destBufferSize); } // //////////////////////////////////////////////////////////////// // // STEP 6.bis // ---------- // Kakadu items deallocation/finalization // //////////////////////////////////////////////////////////////// decompressor.Finish(); decompressor.Native_destroy(); codestream.Destroy(); // //////////////////////////////////////////////////////////////// // // STEP 7. // ------- // BufferedImage Creation // // //////////////////////////////////////////////////////////////// final SampleModel sampleModel = sm.createCompatibleSampleModel( requiredRegion.width, requiredRegion.height); bi = new BufferedImage(cm, Raster.createWritableRaster(sampleModel, imageBuffer, null), false, null); } catch (KduException e) { throw new RuntimeException( "Error caused by a Kakadu exception during creation of key objects! ", e); } catch (RasterFormatException rfe) { throw new RuntimeException("Error during raster creation", rfe); } finally { if (!isRawSource) { try { if (localWrappedSource.Exists()) localWrappedSource.Close(); } catch (KduException e) { } localWrappedSource.Native_destroy(); try { if (localFamilySource.Exists()) localFamilySource.Close(); } catch (KduException e) { } localFamilySource.Native_destroy(); } else { localRawSource.Native_destroy(); } if (deleteInputFile && inputFile.exists()) { inputFile.delete(); } } // //////////////////////////////////////////////////////////////// // // STEP 8 (optional). // ------------------ // Image resampling if needed (See STEP 3 for additional info) // // //////////////////////////////////////////////////////////////// if (resamplingIsRequired && bi != null) return KakaduUtilities.subsampleImage(codestreamP.getColorModel(), bi, destinationRegion.width, destinationRegion.height,interpolationType); return bi; } /** * Checks on the required region and mapped region, in compliance with * Formula 11.1 of Taubman's JPEG2000 compression book. * * @param dims * dimensions of the image region in the actual resolution level * @param requiredRegion * The required region * @throws KduException */ private void checkBounds(final Kdu_dims dims, final Rectangle requiredRegion) throws KduException { if (dims == null) throw new IllegalArgumentException("Provided Kdu_dims object is null"); if (requiredRegion == null) throw new IllegalArgumentException("Provided region is null"); final int mappedWidth = dims.Access_size().Get_x(); final int mappedHeight = dims.Access_size().Get_y(); if (requiredRegion.y + requiredRegion.height > mappedHeight) requiredRegion.height--; if (requiredRegion.x + requiredRegion.width > mappedWidth) requiredRegion.width --; } /** * Set a Kdu_dims object to crop the required region. */ private void setDimsForCrop(final Kdu_dims dims, final Rectangle requiredRegion) throws KduException { if (dims == null) throw new IllegalArgumentException("Provided Kdu_dims object is null"); if (requiredRegion == null) throw new IllegalArgumentException("Provided region is null"); dims.Access_pos().Set_x(dims.Access_pos().Get_x() + requiredRegion.x); dims.Access_pos().Set_y(dims.Access_pos().Get_y() + requiredRegion.y); dims.Access_size().Set_x(requiredRegion.width); dims.Access_size().Set_y(requiredRegion.height); } /** * Parses the input <code>ImageReadParam</code> and provides to setup a * proper sourceRegion and a proper destinationRegion. After the call of * this method, the input parameters <code>sourceRegion</code> and * <code>destinationRegion</code> will contain the computed regions. * Region computations include adjusting edges and subsampling factors * evaluation for the determination of the destination region. * * @param width * the original image width * @param height * the original image height * @param param * an <code>ImageReadParam</code> specifying subsampling * factors and, optionally, a user specified source region. * @param sourceRegion * a <code>Rectangle</code> which will contain the source * region to be used in the read operation. * @param destinationRegion * a <code>Rectangle</code> which will contain the computed * destination region. */ private static void computeRegions(final int width, final int height, final ImageReadParam param, final Rectangle sourceRegion, final Rectangle destinationRegion) { // // // // Getting input parameters // // // final Rectangle paramRegion = param.getSourceRegion(); final int xSubsamplingFactor = param.getSourceXSubsampling(); final int ySubsamplingFactor = param.getSourceYSubsampling(); if (paramRegion != null) { sourceRegion.setBounds(paramRegion); // //////////////////////////////////////////////////////////////// // // Minimum correction for wrong source regions // // When you do subsampling or source subsetting it might happen that // the given source region in the read param is uncorrect, which // means it can be or a bit larger than the original file or can // begin a bit before original limits. // // We got to be prepared to handle such case in order to avoid // generating ArrayIndexOutOfBoundsException later in the code. // // //////////////////////////////////////////////////////////////// if (sourceRegion.x < 0) sourceRegion.x = 0; if (sourceRegion.y < 0) sourceRegion.y = 0; // initializing destination image properties destinationRegion.x = sourceRegion.x; if ((sourceRegion.x + sourceRegion.width) > width) { sourceRegion.width = width - sourceRegion.x; } destinationRegion.width = sourceRegion.width; destinationRegion.y = sourceRegion.y; if ((sourceRegion.y + sourceRegion.height) > height) { sourceRegion.height = height - sourceRegion.y; } destinationRegion.height = sourceRegion.height; } else { // Source Region not specified. // Assuming Source Region Dimension equal to Source Image Dimension destinationRegion.setBounds(0, 0, width, height); sourceRegion.setBounds(0, 0, width, height); } // //////////////////////////////////////////////////////////////////// // // Updating the destination size in compliance with the subSampling // parameters // // //////////////////////////////////////////////////////////////////// destinationRegion.width = ((destinationRegion.width - 1) / xSubsamplingFactor) + 1; destinationRegion.height = ((destinationRegion.height - 1) / ySubsamplingFactor) + 1; if (sourceRegion.x != 0) { destinationRegion.x = sourceRegion.x / xSubsamplingFactor; } if (sourceRegion.y != 0) { destinationRegion.y = sourceRegion.y / ySubsamplingFactor; } } /** * Computes the set of parameters to be used by the * {@code Kdu_stripe_decompressor} in the next operation. Mainly, given a * specified sourceRegion, a destinationRegion and a set of x/y subsampling * factors, it allows to setup a requiredRegion, the optimal resolution to * be used as parameters of the stripe decompressor. Moreover, it decides if * the image coming from the stripe decompressor should be resampled. * Resampling is usually needed when requesting asymmetric subsampling * factors (x != y) or when requesting a subsampling factor which isn't a * power of 2. * * @param maxSupportedSubSamplingFactor * the max supported subsamplingfactor * @param xSubsamplingFactor * the requested subsamplingfactor along x * @param ySubsamplingFactor * the requested subsamplingfactor along y * @param sourceRegion * the input source region * @param destinationRegion * the desired output region * @param requiredRegion * a suitable region related to the data values which will be * obtained by the stripe decompressor * @param resolutionInfo * an {@code int[]} containing: the optimal subsampling * factor at index 0; the number of DWT levels to be * discarded at index 1 * @return {@code true} in case the data obtained by the stripe decompressor * in the following operation need to be resampled. {@code false} in * case the data is ready to be used. */ private boolean getRequiredRegionsAndResolutions( final JP2KCodestreamProperties codestreamP, final int xSubsamplingFactor, final int ySubsamplingFactor, final Rectangle sourceRegion, final Rectangle destinationRegion, final Rectangle requiredRegion, final int resolutionInfo[]) { boolean resamplingIsRequired = false; int newSubSamplingFactor = 0; final int maxSupportedSubSamplingFactor = codestreamP .getMaxSupportedSubSamplingFactor(); // Preliminar check: Are xSSF and ySSF different? final boolean subSamplingFactorsAreDifferent = (xSubsamplingFactor != ySubsamplingFactor); // Let be nSSF the minimum of xSSF and ySSF (They may be equals). newSubSamplingFactor = (xSubsamplingFactor <= ySubsamplingFactor) ? xSubsamplingFactor : ySubsamplingFactor; // if nSSF is greater than the maxSupportedSubSamplingFactor // (MaxSupSSF), it needs to be adjusted. final boolean changedSubSamplingFactors = (newSubSamplingFactor > maxSupportedSubSamplingFactor); if (newSubSamplingFactor > maxSupportedSubSamplingFactor) newSubSamplingFactor = maxSupportedSubSamplingFactor; final int info[] = KakaduUtilities.findOptimalResolutionInfo( codestreamP.getSourceDWTLevels(), newSubSamplingFactor); resamplingIsRequired = subSamplingFactorsAreDifferent || changedSubSamplingFactors || info[0] != newSubSamplingFactor; if (!resamplingIsRequired) { // xSSF and ySSF are equal and they are not greater than MaxSuppSSF requiredRegion.setBounds(destinationRegion); } else { // xSSF and ySSF are different or they are greater than MaxSuppSFF. // We need to find a new subsampling factor to load a proper region. newSubSamplingFactor = info[0]; requiredRegion.width = ((sourceRegion.width - 1) / newSubSamplingFactor) + 1; requiredRegion.height = ((sourceRegion.height - 1) / newSubSamplingFactor) + 1; if (sourceRegion.x != 0) { requiredRegion.x = sourceRegion.x / newSubSamplingFactor; } if (sourceRegion.y != 0) { requiredRegion.y = sourceRegion.y / newSubSamplingFactor; } } resolutionInfo[0] = newSubSamplingFactor; resolutionInfo[1] = info[1]; return resamplingIsRequired; } public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { // // // // Reset the reader prior to do anything with it // // // reset(); // // // // Check the input // // // if (input == null) throw new NullPointerException("The provided input is null!"); // Checking if the provided input is a File if (input instanceof File) { inputFile = (File) input; // Checking if the provided input is a FileImageInputStreamExt } else if (input instanceof FileImageInputStreamExt) { inputFile = ((FileImageInputStreamExt) input).getFile(); // Checking if the provided input is a URL } else if (input instanceof URL) { final URL tempURL = (URL) input; if (tempURL.getProtocol().equalsIgnoreCase("file")) { inputFile = Utilities.urlToFile(tempURL); } } else if (input instanceof ImageInputStream) { try { inputFile = File.createTempFile("buffer", ".j2c"); FileOutputStream fos = new FileOutputStream(inputFile); final byte buff[] = new byte[TEMP_BUFFER_SIZE]; int bytesRead = 0; while ((bytesRead = ((ImageInputStream) input).read(buff)) != -1) fos.write(buff, 0, bytesRead); fos.close(); input = inputFile; deleteInputFile = true; } catch (IOException ioe) { throw new RuntimeException("Unable to create a temp file", ioe); } } if (this.inputFile == null) throw new IllegalArgumentException("Invalid source provided."); fileName = inputFile.getAbsolutePath(); // // // // Open it up // // // Kdu_simple_file_source rawSource = null; // Must be disposed last final Jp2_family_src familySource = new Jp2_family_src(); // Dispose // last final Jpx_source wrappedSource = new Jpx_source(); // Dispose in the // middle Kdu_codestream codestream = new Kdu_codestream(); try { // Open input file as raw codestream or a JP2/JPX file familySource.Open(fileName); final int success = wrappedSource.Open(familySource, true); if (success < 0) { // // // // Must open as raw file // // // familySource.Close(); wrappedSource.Close(); rawSource = new Kdu_simple_file_source(fileName); if (rawSource != null) { if (fileName != null){ FileInputStream fis = new FileInputStream (new File(fileName)); byte[] jp2SocMarker = new byte[2]; fis.read(jp2SocMarker); if (jp2SocMarker[0] != (byte)0xFF || jp2SocMarker[1] != (byte) 0x4F) throw new IllegalArgumentException("Not a JP2K source."); fis.close(); } isRawSource = true; if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Detected raw source"); numImages = 1; } } else { // we have a valid jp2/jpx file isRawSource = false; // // // // get the number of codestreams in this jpeg2000 file // // // final int[] count = new int[1]; if (wrappedSource.Count_codestreams(count)) numImages = count[0]; else numImages = 0; } if (!isRawSource) fileWalker = new JP2KFileWalker(this.fileName); for (int cs = 0; cs < numImages; cs++) { if (isRawSource) { codestream.Create(rawSource); } else { final Jpx_codestream_source stream = wrappedSource .Access_codestream(cs); final Jpx_input_box inputbox = stream.Open_stream(); codestream.Create(inputbox); } final JP2KCodestreamProperties codestreamP = new JP2KCodestreamProperties(); // // // // Initializing size-related properties (Image dims, Tiles) // // // final Kdu_dims imageDims = new Kdu_dims(); codestream.Access_siz().Finalize_all(); codestream.Get_dims(-1, imageDims, false); final Kdu_dims tileSize = new Kdu_dims(); codestream.Get_tile_dims(new Kdu_coords(0, 0), -1, tileSize); int tileWidth = tileSize.Access_size().Get_x(); int tileHeight = tileSize.Access_size().Get_y(); // Tuning tiles in case the tile size is greater than the // maximum integer if ((float) tileWidth * tileHeight >= Integer.MAX_VALUE) { // TODO: Customize these settings tileHeight = 1024; tileWidth = 1024; } codestreamP.setTileWidth(tileWidth); codestreamP.setTileHeight(tileHeight); codestreamP.setWidth(imageDims.Access_size().Get_x()); codestreamP.setHeight(imageDims.Access_size().Get_y()); final int nComponents = codestream.Get_num_components(); int maxBitDepth = -1; int[] componentIndexes = new int[nComponents]; int[] bitsPerComponent = new int[nComponents]; boolean isSigned = false; for (int i = 0; i < nComponents; i++) { // TODO: FIX THIS bitsPerComponent[i] = codestream.Get_bit_depth(i); if (maxBitDepth < bitsPerComponent[i]) { maxBitDepth = bitsPerComponent[i]; } isSigned |= codestream.Get_signed(i); componentIndexes[i] = i; } codestreamP.setNumComponents(nComponents); codestreamP.setBitsPerComponent(bitsPerComponent); codestreamP.setComponentIndexes(componentIndexes); codestreamP.setMaxBitDepth(maxBitDepth); codestreamP.setSigned(isSigned); // Initializing Resolution levels and Quality Layers final int sourceDWTLevels = codestream.Get_min_dwt_levels(); codestreamP.setSourceDWTLevels(sourceDWTLevels); codestreamP .setMaxSupportedSubSamplingFactor(1 << sourceDWTLevels); Kdu_coords tileCoords = new Kdu_coords(); tileCoords.Set_x(0); tileCoords.Set_y(0); codestream.Open_tile(tileCoords); codestreamP.setMaxAvailableQualityLayers(codestream .Get_max_tile_layers()); initializeSampleModelAndColorModel(codestreamP); codestream.Destroy(); multipleCodestreams.add(codestreamP); if (isRawSource) break; } } catch (KduException e) { throw new RuntimeException( "Error caused by a Kakadu exception during creation of key objects!" ,e); } catch (FileNotFoundException e) { throw new RuntimeException( "Exception occurred during kakadu reader initialization", e); } catch (IOException e) { throw new RuntimeException( "Exception occurred during kakadu reader initialization", e); } finally { if (!isRawSource && wrappedSource != null) { try { if (wrappedSource.Exists()) wrappedSource.Close(); } catch (Throwable e) { // yeah I am eating this } try { wrappedSource.Native_destroy(); } catch (Throwable e) { // yeah I am eating this } if (familySource != null) { try { if (familySource.Exists()) familySource.Close(); } catch (Throwable e) { // yeah I am eating this } try { familySource.Native_destroy(); } catch (Throwable e) { // yeah I am eating this } } } else if (isRawSource && rawSource != null) { try { if (wrappedSource.Exists()) wrappedSource.Close(); } catch (Throwable e) { // yeah I am eating this } } } // // // // Setting input for superclass // // // super.setInput(input, seekForwardOnly, ignoreMetadata); } /** * Disposes all the resources, native and non, used by this * {@link ImageReader} subclass. */ public void dispose() { super.dispose(); if (multipleCodestreams != null) { multipleCodestreams.clear(); } numImages = 1; fileWalker = null; } /** * Returns the height of a tile * * @param the * index of the selected image */ public int getTileHeight(int imageIndex) throws IOException { checkImageIndex(imageIndex); final int tileHeight = multipleCodestreams.get(imageIndex) .getTileHeight(); if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine(new StringBuffer("tileHeight:").append( Integer.toString(tileHeight)).toString()); return tileHeight; } /** * Returns the width of a tile * * @param the * index of the selected image */ public int getTileWidth(int imageIndex) throws IOException { checkImageIndex(imageIndex); final int tileWidth = multipleCodestreams.get(imageIndex) .getTileWidth(); if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine(new StringBuffer("tileWidth:").append( Integer.toString(tileWidth)).toString()); return tileWidth; } /** * Initialize SampleModel and ColorModel * * @throws KduException */ private synchronized void initializeSampleModelAndColorModel( JP2KCodestreamProperties codestreamP) throws KduException { if (codestreamP.getSampleModel() != null && codestreamP.getColorModel() != null) return; parseBoxes(codestreamP); if (codestreamP.getColorModel() == null) codestreamP.setColorModel(getColorModel(codestreamP)); if (codestreamP.getSampleModel() == null) codestreamP.setSampleModel(getSampleModel(codestreamP)); } /** * Setup a proper <code>ColorModel</code> * * @return a color model. * @throws KduException * */ private static ColorModel getColorModel(JP2KCodestreamProperties codestreamP) throws KduException { if (codestreamP.getColorModel() != null) return codestreamP.getColorModel(); if (codestreamP.getColorModel() != null) return codestreamP.getColorModel(); final int nComponents = codestreamP.getNumComponents(); if (nComponents <= 4) { ColorSpace cs; if (nComponents > 2) { cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); } else { cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); } boolean hasAlpha = nComponents % 2 == 0; final int maxBitDepth = codestreamP.getMaxBitDepth(); if (maxBitDepth <= 8) { codestreamP.setDataBufferType(DataBuffer.TYPE_BYTE); } else if (maxBitDepth <= 16) { if (codestreamP.isSigned()) codestreamP.setDataBufferType(DataBuffer.TYPE_SHORT); else codestreamP.setDataBufferType(DataBuffer.TYPE_USHORT); } else if (maxBitDepth <= 32) { codestreamP.setDataBufferType(DataBuffer.TYPE_INT); } final int dataBufferType = codestreamP.getDataBufferType(); if (dataBufferType != -1) { if (nComponents == 1 && (maxBitDepth == 1 || maxBitDepth == 2 || maxBitDepth == 4)) { codestreamP.setColorModel(ImageUtil .createColorModel(getSampleModel(codestreamP))); } else { codestreamP.setColorModel(new ComponentColorModel(cs, codestreamP.getBitsPerComponent(), hasAlpha, false, hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, dataBufferType)); } return codestreamP.getColorModel(); } } if (codestreamP.getSampleModel() == null) codestreamP.setSampleModel(getSampleModel(codestreamP)); if (codestreamP.getSampleModel() == null) return null; return ImageUtil.createColorModel(codestreamP.getSampleModel()); } /** * Get basic image properties by querying several JP2Boxes. Then, properly * set the ColorModel of the input object. * * @param codestreamP */ private void parseBoxes(JP2KCodestreamProperties codestreamP) { if (isRawSource) return; short numComp = 1; byte[] bitDepths = null; byte[] maps = null; int bitDepth = -1; ICC_Profile profile = null; int colorSpaceType = -1; // // // // ImageHeader Box // // // final ImageHeaderBox ihBox = (ImageHeaderBox) getJp2Box(ImageHeaderBox.BOX_TYPE); if (ihBox != null) { numComp = ihBox.getNumComponents(); bitDepth = ihBox.getBitDepth(); } // // // // ColorSpecification Box // // // final ColorSpecificationBox csBox = (ColorSpecificationBox) getJp2Box(ColorSpecificationBox.BOX_TYPE); if (csBox != null) { profile = csBox.getICCProfile(); colorSpaceType = csBox.getEnumeratedColorSpace(); } // // // // ComponentMapping Box // // // final ComponentMappingBox cmBox = (ComponentMappingBox) getJp2Box(ComponentMappingBox.BOX_TYPE); if (cmBox != null) { maps = cmBox.getComponentAssociation(); } // // // // Palette Box // // // final PaletteBox palBox = (PaletteBox) getJp2Box(PaletteBox.BOX_TYPE); if (palBox != null) { byte[][] lookUpTable = palBox.getLUT(); if (lookUpTable != null && numComp == 1) { int tableComps = lookUpTable.length; int maxDepth = 1 + (bitDepth & 0x7F); if (maps == null) { maps = new byte[tableComps]; for (int i = 0; i < tableComps; i++) maps[i] = (byte) i; } if (tableComps == 3) { codestreamP.setColorModel(new IndexColorModel(maxDepth, lookUpTable[0].length, lookUpTable[maps[0]], lookUpTable[maps[1]], lookUpTable[maps[2]])); return; } else if (tableComps == 4) { codestreamP.setColorModel(new IndexColorModel(maxDepth, lookUpTable[0].length, lookUpTable[maps[0]], lookUpTable[maps[1]], lookUpTable[maps[2]], lookUpTable[maps[3]])); return; } } } // // // // BitsPerComponent Box // // // final BitsPerComponentBox bpcBox = (BitsPerComponentBox) getJp2Box(BitsPerComponentBox.BOX_TYPE); if (bpcBox != null) { bitDepths = bpcBox.getBitDepth(); } // // // // ChannelDefinition Box // // // final ChannelDefinitionBox chBox = (ChannelDefinitionBox) getJp2Box(ChannelDefinitionBox.BOX_TYPE); if (chBox != null) { final short[] channels = chBox.getChannel(); final short[] associations = chBox.getAssociation(); final int[] cType = chBox.getTypes(); boolean hasAlpha = false; final int alphaChannel = numComp - 1; for (int i = 0; i < channels.length; i++) { if (cType[i] == 1 && channels[i] == alphaChannel) hasAlpha = true; } boolean[] isPremultiplied = new boolean[] { false }; if (hasAlpha) { isPremultiplied = new boolean[alphaChannel]; for (int i = 0; i < alphaChannel; i++) isPremultiplied[i] = false; for (int i = 0; i < channels.length; i++) { if (cType[i] == 2) isPremultiplied[associations[i] - 1] = true; } for (int i = 1; i < alphaChannel; i++) isPremultiplied[0] &= isPremultiplied[i]; } ColorSpace cs = null; //RGBN Workaround if (associations.length == 4/* && associations[0]==1 && associations[1]==2 && associations[2]==3*/){ cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); hasAlpha = true; } else if (profile != null) cs = new ICC_ColorSpace(profile); else if (colorSpaceType == ColorSpecificationBox.ECS_sRGB) cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); else if (colorSpaceType == ColorSpecificationBox.ECS_GRAY) cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); else if (colorSpaceType == ColorSpecificationBox.ECS_YCC) cs = ColorSpace.getInstance(ColorSpace.CS_PYCC); else LOGGER.warning("JP2 type only handle sRGB, GRAY and YCC Profiles"); // TODO: Check these settings int[] bits = new int[numComp]; for (int i = 0; i < numComp; i++) if (bitDepths != null) bits[i] = (bitDepths[i] & 0x7F) + 1; else bits[i] = (bitDepth & 0x7F) + 1; int maxBitDepth = 1 + (bitDepth & 0x7F); boolean isSigned = (bitDepth & 0x80) == 0x80; if (bitDepths != null) isSigned = (bitDepths[0] & 0x80) == 0x80; if (bitDepths != null) for (int i = 0; i < numComp; i++) if (bits[i] > maxBitDepth) maxBitDepth = bits[i]; int type = -1; if (maxBitDepth <= 8) type = DataBuffer.TYPE_BYTE; else if (maxBitDepth <= 16) type = isSigned ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT; else if (maxBitDepth <= 32) type = DataBuffer.TYPE_INT; if (type == -1) return; if (cs != null) { codestreamP.setColorModel(new ComponentColorModel(cs, bits, hasAlpha, isPremultiplied[0], hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, type)); } } } /** * Setup a proper <code>SampleModel</code> * * @return a sample model. * @throws KduException */ private static SampleModel getSampleModel(JP2KCodestreamProperties codestreamP) { if (codestreamP == null) throw new IllegalArgumentException( "null codestream properties provided"); if (codestreamP.getSampleModel() != null) return codestreamP.getSampleModel(); final int nComponents = codestreamP.getNumComponents(); final int maxBitDepth = codestreamP.getMaxBitDepth(); final int tileWidth = codestreamP.getTileWidth(); final int tileHeight = codestreamP.getTileHeight(); if (nComponents == 1 && (maxBitDepth == 1 || maxBitDepth == 2 || maxBitDepth == 4)) codestreamP.setSampleModel(new MultiPixelPackedSampleModel( DataBuffer.TYPE_BYTE, tileWidth, tileHeight, maxBitDepth)); else if (maxBitDepth <= 8) codestreamP .setSampleModel(new PixelInterleavedSampleModel( DataBuffer.TYPE_BYTE, tileWidth, tileHeight, nComponents, tileWidth * nComponents, codestreamP .getComponentIndexes())); else if (maxBitDepth <= 16) codestreamP.setSampleModel(new PixelInterleavedSampleModel( codestreamP.isSigned() ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT, tileWidth, tileHeight, nComponents, tileWidth * nComponents, codestreamP .getComponentIndexes())); else if (maxBitDepth <= 32) codestreamP .setSampleModel(new PixelInterleavedSampleModel( DataBuffer.TYPE_INT, tileWidth, tileHeight, nComponents, tileWidth * nComponents, codestreamP .getComponentIndexes())); else throw new IllegalArgumentException("Unhandled sample model"); return codestreamP.getSampleModel(); } /** * Build a default {@link JP2KKakaduImageReadParam} */ public ImageReadParam getDefaultReadParam() { return new JP2KKakaduImageReadParam(); } /** * returns the number of source DWT levels for the specified image. * * @return the number of source DWT levels. */ public int getSourceDWTLevels(int imageIndex) { checkImageIndex(imageIndex); return multipleCodestreams.get(imageIndex).getSourceDWTLevels(); } /** * Returns a {@link JP2KBox} given the boxType name. * * @param boxType * the type of required Jp2 Box * @return a {@link JP2KBox} given the boxType name. Return * <code>null</code> if not found. */ private JP2KBox getJp2Box(final String boxType) { final TreeModel boxesTree = fileWalker.getJP2KBoxesTree(); DefaultMutableTreeNode root = (DefaultMutableTreeNode) boxesTree.getRoot(); DefaultMutableTreeNode node = null; if (root != null) for (Enumeration e = root.breadthFirstEnumeration(); e.hasMoreElements();) { final DefaultMutableTreeNode current = (DefaultMutableTreeNode) e.nextElement(); final JP2KBox box = (JP2KBox) current; if (box != null && BoxUtilities.getTypeInt(boxType) == box.getType()) { node = current; break; } } if (node != null) { return LazyJP2KBox.getAsOriginalBox((JP2KBox) node); } return null; } /** * Returns a proper {@link JP2KBox} subclass instance given a specified box * type. * * @param boxType * @return a proper */ private JP2KBox getJp2Box(final int boxType) { return getJp2Box(BoxUtilities.getBoxName(boxType)); } /** * Reset main values */ public void reset() { super.setInput(null, false, false); dispose(); numImages = -1; isRawSource = false; } File getInputFile() { return inputFile; } String getFileName() { return fileName; } }