/* JP2K Kakadu Image Writer V. 1.0 * * (c) 2008 Quality Nighthawk Teleradiology Group, Inc. * Contact: info@qualitynighthawk.com * * Produced by GeoSolutions, Eng. Daniele Romagnoli and Eng. Simone Giannecchini * GeoSolutions S.A.S. --- Via Carignoni 51, 55041 Camaiore (LU) Italy * Contact: info@geo-solutions.it * * Released under the Gnu Lesser General Public License version 3. * All rights otherwise reserved. * * JP2K Kakadu Image Writer is distributed on an "AS IS" basis, * WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU Lesser General Public License version 3 for more details. * http://www.fsf.org/licensing/licenses/lgpl.html */ package it.geosolutions.imageio.plugins.jp2k; import it.geosolutions.imageio.plugins.jp2k.JP2KKakaduImageWriteParam.Compression; import it.geosolutions.imageio.plugins.jp2k.JP2KKakaduImageWriteParam.ProgressionOrder; import it.geosolutions.imageio.plugins.jp2k.box.UUIDBox; import it.geosolutions.imageio.stream.output.FileImageOutputStreamExt; import it.geosolutions.imageio.utilities.ImageIOUtilities; import it.geosolutions.util.KakaduUtilities; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.color.ColorSpace; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URL; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.IIOImage; import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.stream.ImageOutputStream; import kdu_jni.Jp2_channels; import kdu_jni.Jp2_colour; import kdu_jni.Jp2_dimensions; import kdu_jni.Jp2_family_tgt; import kdu_jni.Jp2_palette; import kdu_jni.Jp2_target; import kdu_jni.KduException; import kdu_jni.Kdu_codestream; import kdu_jni.Kdu_compressed_target; import kdu_jni.Kdu_global; import kdu_jni.Kdu_params; import kdu_jni.Kdu_simple_file_target; import kdu_jni.Kdu_stripe_compressor; import kdu_jni.Siz_params; /** * @author Daniele Romagnoli, GeoSolutions * @author Simone Giannecchini, GeoSolutions */ public class JP2KKakaduImageWriter extends ImageWriter { private final static short[] GEOJP2_UUID = new short[] { 0xb1, 0x4b, 0xf8, 0xbd, 0x08, 0x3d, 0x4b, 0x43, 0xa5, 0xae, 0x8c, 0xd7, 0xd5, 0xa6, 0xce, 0x03 }; private final static int POWERS_2[] = new int[] { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216 }; /** The LOGGER for this class. */ private static final Logger LOGGER = Logger.getLogger("it.geosolutions.imageio.plugins.jp2k"); /** The System Property key used to define the maximum buffer size */ public final static String MAX_BUFFER_SIZE_KEY = "it.geosolutions.maxBufferSize"; /** The System Property key used to define the temp buffer size */ public final static String TEMP_BUFFER_SIZE_KEY = "it.geosolutions.tempBufferSize"; /** The System Property key used to customize the Comment Marker presence */ public final static String ADD_COMMENT_MARKER_KEY = "it.geosolutions.addCommentMarker"; /** The Default Maximum Buffer Size */ private final static int DEFAULT_MAX_BUFFER_SIZE = 32 * 1024 * 1024; /** The Default Temp Buffer Size */ private final static int DEFAULT_TEMP_BUFFER_SIZE = 64 * 1024; /** * Set at once to indicate whether to write the comment marker generated by * rate allocation */ private final static boolean ADD_COMMENT_MARKER; /** * A temporary buffer size where to store data when directly writing * on an output stream */ private static int TEMP_BUFFER_SIZE = DEFAULT_TEMP_BUFFER_SIZE; /** * The max size in bytes of the buffer to be used by stripe compressor. */ private final static int MAX_BUFFER_SIZE; private final static int MIN_BUFFER_SIZE = 1024 * 1024; /** * Sizes of Markers */ private final static class MarkerSize { final static int SOC = 2; final static int COD = 14; final static int SOT = 12; final static int EOC = 2; final static int SOD = 2; final static int COM = 108; final static int TLM = 8; // Value FF55 + L_TLM bytes + I_TLM + L final static int COM_KAKADUV = 17; final static int QCD_ESTIMATE = 40; } private ImageOutputStream outputStream = null; /** * Static initialization */ static { int size = DEFAULT_MAX_BUFFER_SIZE; int buffer = DEFAULT_TEMP_BUFFER_SIZE; final Integer maxSize = Integer.getInteger(MAX_BUFFER_SIZE_KEY); final Integer bufferSize = Integer.getInteger(TEMP_BUFFER_SIZE_KEY); String marker = System.getProperty(ADD_COMMENT_MARKER_KEY); if (marker != null) { final Boolean addComment = Boolean.parseBoolean(ADD_COMMENT_MARKER_KEY); if (addComment != null){ ADD_COMMENT_MARKER = addComment.booleanValue(); } else { ADD_COMMENT_MARKER = true; } } else { ADD_COMMENT_MARKER = true; } // // // // Setting MAX BUFFER SIZE // // // if (maxSize != null) { size = maxSize.intValue(); } else { final String maxSizes = System.getProperty(MAX_BUFFER_SIZE_KEY); if (maxSizes != null) { size = parseSize(maxSizes); } } // // // // Setting TEMP BUFFER SIZE // // // if (bufferSize != null) { buffer = bufferSize.intValue(); } else { final String bufferSizes = System.getProperty(TEMP_BUFFER_SIZE_KEY); if (bufferSizes != null) { buffer = parseSize(bufferSizes); } } TEMP_BUFFER_SIZE = buffer; MAX_BUFFER_SIZE = size; } /** * Get a default {@link ImageWriteParam} instance. */ public ImageWriteParam getDefaultWriteParam() { return new JP2KKakaduImageWriteParam(); } /** * * @param sizeValue * @return */ private static int parseSize(final String sizeValue) { // // // // Checking for a properly formatted string value. // Valid values should end with one of M,m,K,k // // // int size = 0; final int length = sizeValue.length(); final String value = sizeValue.substring(0, length - 1); final String suffix = sizeValue.substring(length - 1, length); // // // // Checking for valid multiplier suffix // // // if (suffix.equalsIgnoreCase("M") || suffix.equalsIgnoreCase("K")) { int val; try { val = Integer.parseInt(value); if (suffix.equalsIgnoreCase("M")) val *= (1024 * 1024); // Size in MegaBytes else val *= 1024; // Size in KiloBytes size = val; } catch (NumberFormatException nfe) { // not a valid value } } return size; } /** * In case the ratio between the stripe_height and the image height is * greater than this value, set the stripe_height to the image height in * order to do a single push */ private static final double SINGLE_PUSH_THRESHOLD_RATIO = 0.95; /** * The file to be written */ private File outputFile; public JP2KKakaduImageWriter(ImageWriterSpi originatingProvider) { super(originatingProvider); } @Override public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { return null; } @Override public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) { return null; } @Override public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { return null; } @Override public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { return null; } /** * Sets the destination to the given <code>Object</code>, usually a * <code>File</code> or a {@link FileImageOutputStreamExt}. * * @param output * the <code>Object</code> to use for future writing. */ public void setOutput(Object output) { super.setOutput(output); // validates output if (output instanceof File) outputFile = (File) output; else if (output instanceof FileImageOutputStreamExt) outputFile = ((FileImageOutputStreamExt) output).getFile(); else if (output instanceof URL) { final URL tempURL = (URL) output; if (tempURL.getProtocol().equalsIgnoreCase("file")) { outputFile = ImageIOUtilities.urlToFile(tempURL); } } else if (output instanceof ImageOutputStream) { try { outputStream = (ImageOutputStream) output; outputFile = File.createTempFile("buffer", ".j2c"); } catch (IOException e) { throw new RuntimeException("Unable to create a temp file", e); } } } @Override public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException { // //////////////////////////////////////////////////////////////////// // // Variables initialization // // //////////////////////////////////////////////////////////////////// final String fileName = outputFile.getAbsolutePath(); JP2KKakaduImageWriteParam jp2Kparam; final boolean writeCodeStreamOnly; final double quality; int cLayers = 1; int cLevels; final boolean cycc; byte[] geoJp2 = null; final boolean orgGen_plt; int orgGen_tlm = JP2KKakaduImageWriteParam.UNSPECIFIED_ORG_GEN_TLM; int qGuard = -1; String orgT_parts = null; String cPrecincts = null; boolean setTiling = false; int tileW = Integer.MIN_VALUE; int tileH = Integer.MIN_VALUE; ProgressionOrder cOrder = null; double[] bitRates = null; boolean addCommentMarker = ADD_COMMENT_MARKER; int sProfile = JP2KKakaduImageWriteParam.DEFAULT_SPROFILE; Compression compression = Compression.UNDEFINED; // // // // Write parameters parsing // // // if (param == null) { param = getDefaultWriteParam(); } if (param instanceof JP2KKakaduImageWriteParam) { jp2Kparam = (JP2KKakaduImageWriteParam) param; writeCodeStreamOnly = jp2Kparam.isWriteCodeStreamOnly(); bitRates = jp2Kparam.getQualityLayersBitRates(); double q = jp2Kparam.getQuality(); if (q < 0.01) { q = 0.01; if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Quality level should be in the range 0.01 - 1. /n Reassigning it to 0.01"); } quality = q; geoJp2 = jp2Kparam.getGeoJp2(); setTiling = jp2Kparam.getTilingMode() == ImageWriteParam.MODE_EXPLICIT; if (setTiling) { tileH = jp2Kparam.getTileHeight(); tileW = jp2Kparam.getTileWidth(); } // COD PARAMS cOrder = jp2Kparam.getcOrder(); cPrecincts = jp2Kparam.getcPrecincts(); cLevels = jp2Kparam.getCLevels(); cLayers = jp2Kparam.getQualityLayers(); // ORG PARAMS orgGen_plt = jp2Kparam.isOrgGen_plt(); orgGen_tlm = jp2Kparam.getOrgGen_tlm(); orgT_parts = jp2Kparam.getOrgT_parts(); qGuard = jp2Kparam.getqGuard(); addCommentMarker &= jp2Kparam.isAddCommentMarker(); sProfile = jp2Kparam.getsProfile(); compression = jp2Kparam.getCompression(); if (bitRates != null && bitRates.length != cLayers) { throw new IllegalArgumentException(" Specified bitRates parameter's length " + bitRates.length + " should match the quality layers parameter " + "(cLayers): " + cLayers); } if (compression != null){ switch (compression){ case LOSSY: if (bitRates != null){ if (LOGGER.isLoggable(Level.FINE)){ LOGGER.fine("Applying lossy compression leveraging on provided quality bit rates"); } } else { if (LOGGER.isLoggable(Level.FINE)){ LOGGER.fine("Applying lossy compression leveraging on quality factor"); } } break; case NUMERICALLY_LOSSLESS: if (bitRates != null){ if (LOGGER.isLoggable(Level.FINE)){ LOGGER.fine("Applying numerically lossless compression leveraging on " + "provided quality bit rates"); } if (KakaduUtilities.notEqual(bitRates[bitRates.length - 1], 0)){ throw new IllegalArgumentException("Asking for a Numerically Lossless " +"but the last quality layer's bit rate should be 0 " + " instead of " + bitRates[bitRates.length - 1]); } } else { if (LOGGER.isLoggable(Level.FINE)){ LOGGER.fine("Applying numerically lossless compression"); } } break; } } else { compression = Compression.UNDEFINED; } } else { orgGen_plt = false; writeCodeStreamOnly = true; quality = JP2KKakaduImageWriteParam.DEFAULT_QUALITY; cLevels = JP2KKakaduImageWriteParam.DEFAULT_C_LEVELS; } // //////////////////////////////////////////////////////////////////// // // Image properties initialization // // //////////////////////////////////////////////////////////////////// final RenderedImage inputRenderedImage = image.getRenderedImage(); final int sourceWidth = inputRenderedImage.getWidth(); final int sourceHeight = inputRenderedImage.getHeight(); final int sourceMinX = inputRenderedImage.getMinX(); final int sourceMinY = inputRenderedImage.getMinY(); final SampleModel sm = inputRenderedImage.getSampleModel(); final int dataType = sm.getDataType(); final boolean isDataSigned = (dataType != DataBuffer.TYPE_USHORT && dataType != DataBuffer.TYPE_BYTE); final ColorModel colorModel = inputRenderedImage.getColorModel(); final boolean hasPalette = colorModel instanceof IndexColorModel ? true : false; final int[] numberOfBits = colorModel.getComponentSize(); // The number of bytes used by header, markers, boxes int bytesOverHead = 0; // We suppose all bands share the same bitDepth final int bits = numberOfBits[0]; int nComponents = sm.getNumBands(); // Array to store optional look up table entries byte[] reds = null; byte[] greens = null; byte[] blues = null; // // // // Handling paletted Image // // // if (hasPalette) { cycc = false; cLevels = 1; IndexColorModel icm = (IndexColorModel) colorModel; final int lutSize = icm.getMapSize(); final int numColorComponents = colorModel.getNumColorComponents(); // Updating the number of components to write as RGB (3 bands) if (writeCodeStreamOnly) { nComponents = numColorComponents; // // // // Caching look up table for future accesses. // // // reds = new byte[lutSize]; blues = new byte[lutSize]; greens = new byte[lutSize]; icm.getReds(reds); icm.getGreens(greens); icm.getBlues(blues); } else { // adding pclr and cmap boxes overhead bytes bytesOverHead += (4 + 2 + numColorComponents + 1); // NE + NC + Bi bytesOverHead += lutSize * numColorComponents + 4; // pclr LUT bytesOverHead += 20; // cmap } } else if (quality == 1) { cycc = false; } else { cycc = true; } // // // // Setting regions and sizes and retrieving parameters // // // final int xSubsamplingFactor = param.getSourceXSubsampling(); final int ySubsamplingFactor = param.getSourceYSubsampling(); final Rectangle originalBounds = new Rectangle(sourceMinX, sourceMinY, sourceWidth, sourceHeight); final Rectangle imageBounds = (Rectangle) originalBounds.clone(); final Dimension destSize = new Dimension(); KakaduUtilities.computeRegions(imageBounds, destSize, param); boolean resampleInputImage = false; if (xSubsamplingFactor != 1 || ySubsamplingFactor != 1 || !imageBounds.equals(originalBounds)) { resampleInputImage = true; } // Destination sizes final int destinationWidth = destSize.width; final int destinationHeight = destSize.height; final int rowSize = (destinationWidth * nComponents); final int bandSize = destinationHeight * destinationWidth; final int imageSize = bandSize * nComponents; // //////////////////////////////////////////////////////////////////// // // Kakadu objects initialization // // //////////////////////////////////////////////////////////////////// Kdu_compressed_target outputTarget = null; Jp2_target target = null; Jp2_family_tgt familyTarget = null; // Setting decomposition levels cLevels = setDecompositionLevels(cLevels, destinationWidth); try { if (writeCodeStreamOnly) { // Open a simple file target outputTarget = new Kdu_simple_file_target(); ((Kdu_simple_file_target) outputTarget).Open(fileName); final int extensionIndex = fileName.lastIndexOf("."); final String suffix = fileName.substring(extensionIndex, fileName.length()); if (suffix.equalsIgnoreCase(".jp2") && LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("When writing codestreams, the \".j2c\" file suffix is suggested instead of \".jp2\""); } } else { familyTarget = new Jp2_family_tgt(); familyTarget.Open(fileName); target = new Jp2_target(); target.Open(familyTarget); // Adding signature, fileType, image header, Jp2Header box bytes // + jp2c marker. bytesOverHead += 84; if (geoJp2 != null) { bytesOverHead += geoJp2.length; } } bytesOverHead += addMarkerBytes(nComponents, destinationWidth, destinationHeight, tileW, tileH, orgGen_tlm, addCommentMarker); if (bytesOverHead >= imageSize) { bytesOverHead = 0; } final long qualityLayersSize = bitRates != null ? imageSize : (long) ((imageSize - bytesOverHead) * quality * bits * KakaduUtilities.BIT_TO_BYTE_FACTOR); // // // // Parameters initialization // // // final Kdu_codestream codeStream = new Kdu_codestream(); Siz_params params = new Siz_params(); initializeParams(params, destinationWidth, destinationHeight, bits, nComponents, isDataSigned, tileW, tileH, sProfile); // Create a codestream on the proper output object. if (writeCodeStreamOnly) { codeStream.Create(params, outputTarget, null); } else { codeStream.Create(params, target, null); } if (!initializeCodestream(codeStream, cycc, cLevels, quality, cLayers, colorModel, writeCodeStreamOnly, dataType, target, geoJp2, orgGen_plt, orgGen_tlm, orgT_parts, qGuard, cOrder, cPrecincts, compression)) { throw new IOException("Unable to initialize the codestream due to a missing " + "Jp2_target object"); } // // // // Preparing parameters for stripe compression // // // final Kdu_stripe_compressor compressor = new Kdu_stripe_compressor(); // Array with one entry for each image component, identifying the // number of lines supplied 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[] sampleGaps = 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[] rowGaps = 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[] sampleOffsets = new int[nComponents]; // Array with one entry for each image component, identifying the // number of significant bits used to represent each sample. final int precisions[] = new int[nComponents]; initializeStripeCompressor(compressor, codeStream, quality, cLayers, qualityLayersSize, bitRates, compression, rowSize, destinationHeight, destinationWidth, nComponents, stripeHeights, sampleGaps, rowGaps, sampleOffsets, precisions, numberOfBits, addCommentMarker); // //////////////////////////////////////////////////////////////// // // Pushing Stripes // // //////////////////////////////////////////////////////////////// pushStripes(compressor, inputRenderedImage, imageBounds, originalBounds, isDataSigned, resampleInputImage, writeCodeStreamOnly, hasPalette, nComponents, bits, destinationWidth, destinationHeight, xSubsamplingFactor, ySubsamplingFactor, stripeHeights, sampleGaps, rowGaps, sampleOffsets, precisions, reds, greens, blues); // //////////////////////////////////////////////////////////////// // // Kakadu Objects Finalization // // //////////////////////////////////////////////////////////////// compressor.Finish(); compressor.Native_destroy(); codeStream.Destroy(); } catch (KduException e) { throw (IOException) new IOException("Error caused by a Kakadu exception during write operation").initCause(e); } finally { // // // // Finalize any native object // // // if (!writeCodeStreamOnly && target != null) { try { if (target.Exists()) target.Close(); } catch (Throwable e) { // Does Nothing } try { target.Native_destroy(); } catch (Throwable e) { // Does Nothing } if (familyTarget != null) { try { if (familyTarget.Exists()) familyTarget.Close(); } catch (Throwable e) { // Does Nothing } try { familyTarget.Native_destroy(); } catch (Throwable e) { // Does Nothing } } } else if (writeCodeStreamOnly && outputTarget != null) { try { outputTarget.Close(); } catch (Throwable e) { // Does Nothing } try { outputTarget.Native_destroy(); } catch (Throwable e) { // Does Nothing } } // // // // In case of output stream, write on it using a temp buffer // // // if (outputStream != null) { writeOnStream(); } } } /** * Write back the compressed JP2 file to the provided output stream. * @throws IOException */ private void writeOnStream() throws IOException { final FileInputStream fis = new FileInputStream(outputFile); final byte buff[] = new byte[TEMP_BUFFER_SIZE]; int bytesRead = 0; while ((bytesRead = fis.read(buff)) != -1) { outputStream.write(buff, 0, bytesRead); } outputStream.close(); fis.close(); outputFile.delete(); } /** * Push data stripes from the inputRenderedImage in compliance with the specified parameters. * * @param compressor * @param inputRenderedImage * @param imageBounds * @param originalBounds * @param isDataSigned * @param resampleInputImage * @param writeCodeStreamOnly * @param hasPalette * @param nComponents * @param bits * @param destinationWidth * @param destinationHeight * @param xSubsamplingFactor * @param ySubsamplingFactor * @param stripeHeights * @param sampleGaps * @param rowGaps * @param sampleOffsets * @param precisions * @param reds * @param greens * @param blues * @throws KduException */ private void pushStripes(final Kdu_stripe_compressor compressor, final RenderedImage inputRenderedImage, final Rectangle imageBounds, final Rectangle originalBounds, final boolean isDataSigned, final boolean resampleInputImage, final boolean writeCodeStreamOnly, final boolean hasPalette, final int nComponents, final int bits, final int destinationWidth, final int destinationHeight, final int xSubsamplingFactor, final int ySubsamplingFactor, final int[] stripeHeights, final int[] sampleGaps, final int[] rowGaps, final int[] sampleOffsets, final int precisions[], final byte[] reds, final byte[] greens , final byte[] blues ) throws KduException { boolean goOn = true; final int rowSize = (destinationWidth * nComponents); final int stripeSize = rowSize * stripeHeights[0]; if (bits <= 8) { // // // // Byte Buffer // // // byte[] bufferValues = new byte[stripeSize]; int y = 0; if (!resampleInputImage) { Raster rasterData = null; while (goOn) { // Adjusting the stripeHeight in case of multi-pass // stripes-push, in case the last stripeHeight is too high if (destinationHeight - y < stripeHeights[0]) { for (int i = 0; i < nComponents; i++) { stripeHeights[i] = destinationHeight - y; } bufferValues = new byte[rowSize * stripeHeights[0]]; } rasterData = inputRenderedImage.getData( new Rectangle(0, y, destinationWidth, stripeHeights[0])); if (hasPalette && writeCodeStreamOnly) { DataBuffer buff = rasterData.getDataBuffer(); final int loopLength = buff.getSize(); for (int l = 0; l < loopLength; l++) { int pixel = buff.getElem(l); bufferValues[l * 3] = reds[pixel]; bufferValues[(l * 3) + 1] = greens[pixel]; bufferValues[(l * 3) + 2] = blues[pixel]; } } else { rasterData.getDataElements(0, y, destinationWidth, stripeHeights[0], bufferValues); } goOn = compressor.Push_stripe(bufferValues, stripeHeights, sampleOffsets, sampleGaps, rowGaps, precisions, 0); y += stripeHeights[0]; rasterData = null; } } else { int lastY = imageBounds.y; final int lastX = imageBounds.x + imageBounds.width; ByteBuffer buffer = ByteBuffer.allocate(stripeSize); ByteBuffer data; while (goOn) { if (destinationHeight - y < stripeHeights[0]) { for (int i = 0; i < nComponents; i++) stripeHeights[i] = destinationHeight - y; buffer = ByteBuffer.allocate(rowSize * stripeHeights[0]); } Rectangle rect = new Rectangle(imageBounds.x, lastY, imageBounds.width, stripeHeights[0] * ySubsamplingFactor); rect = rect.intersection(originalBounds); Raster rasterData = inputRenderedImage.getData(rect); lastY = rect.y + rect.height; // SubSampledRead readSubSampled(rect, originalBounds, lastX, lastY, xSubsamplingFactor, ySubsamplingFactor, rasterData, buffer, nComponents); data = buffer; if (hasPalette && writeCodeStreamOnly) { ByteBuffer bb = ByteBuffer.allocate(rowSize * stripeHeights[0]); bufferValues = bb.array(); final int loopLength = buffer.capacity(); for (int l = 0; l < loopLength; l++) { int pixel = buffer.get(); bufferValues[l * 3] = reds[pixel]; bufferValues[(l * 3) + 1] = greens[pixel]; bufferValues[(l * 3) + 2] = blues[pixel]; } data = bb; } goOn = compressor.Push_stripe(data.array(), stripeHeights, sampleOffsets, sampleGaps, rowGaps, precisions, 0); y += stripeHeights[0]; buffer.clear(); data = null; rasterData = null; } } bufferValues = null; } else if (bits > 8 && bits <= 16) { // // // // Short Buffer // // // final boolean[] isSigned = new boolean[nComponents]; for (int i = 0; i < isSigned.length; i++) isSigned[i] = isDataSigned; short[] bufferValues = new short[stripeSize]; int y = 0; if (!resampleInputImage) { while (goOn) { if (destinationHeight - y < stripeHeights[0]) { for (int i = 0; i < nComponents; i++) stripeHeights[i] = destinationHeight - y; bufferValues = new short[rowSize * stripeHeights[0]]; } Raster rasterData = inputRenderedImage .getData(new Rectangle(0, y, destinationWidth, stripeHeights[0])); rasterData.getDataElements(0, y, destinationWidth, stripeHeights[0], bufferValues); goOn = compressor.Push_stripe(bufferValues, stripeHeights, sampleOffsets, sampleGaps, rowGaps, precisions, isSigned, 0); y += stripeHeights[0]; rasterData = null; } } else { final int lastX = imageBounds.x + imageBounds.width; int lastY = imageBounds.y; ShortBuffer buffer = ShortBuffer.allocate(stripeSize); while (goOn) { if (destinationHeight - y < stripeHeights[0]) { for (int i = 0; i < nComponents; i++) stripeHeights[i] = destinationHeight - y; buffer = ShortBuffer.allocate(rowSize * stripeHeights[0]); } Rectangle rect = new Rectangle(imageBounds.x, lastY, imageBounds.width, stripeHeights[0] * ySubsamplingFactor); rect = rect.intersection(originalBounds); Raster rasterData = inputRenderedImage.getData(rect); lastY = rect.y + rect.height; // SubSampledRead readSubSampled(rect, originalBounds, lastX, lastY, xSubsamplingFactor, ySubsamplingFactor, rasterData, buffer, nComponents); goOn = compressor.Push_stripe(buffer.array(), stripeHeights, sampleOffsets, sampleGaps, rowGaps, precisions, isSigned, 0); y += stripeHeights[0]; buffer.clear(); rasterData = null; } } bufferValues = null; } else if (bits > 16 && bits <= 32) { // // // // Int Buffer // // // int[] bufferValues = new int[stripeSize]; int y = 0; while (goOn) { if (!resampleInputImage) { if (destinationHeight - y < stripeHeights[0]) { for (int i = 0; i < nComponents; i++) stripeHeights[i] = destinationHeight - y; bufferValues = new int[rowSize * stripeHeights[0]]; } Raster rasterData = inputRenderedImage.getData( new Rectangle(0, y, destinationWidth, stripeHeights[0])); rasterData.getDataElements(0, y, destinationWidth, stripeHeights[0], bufferValues); goOn = compressor.Push_stripe(bufferValues, stripeHeights, sampleOffsets, sampleGaps, rowGaps, precisions); y += stripeHeights[0]; rasterData = null; } else { int lastY = imageBounds.y; final int lastX = imageBounds.x + imageBounds.width; IntBuffer buffer = IntBuffer.allocate(stripeSize); while (goOn) { if (destinationHeight - y < stripeHeights[0]) { for (int i = 0; i < nComponents; i++) stripeHeights[i] = destinationHeight - y; buffer = IntBuffer.allocate(rowSize * stripeHeights[0]); } Rectangle rect = new Rectangle(imageBounds.x, lastY, imageBounds.width, stripeHeights[0] * ySubsamplingFactor); rect = rect.intersection(originalBounds); Raster rasterData = inputRenderedImage.getData(rect); lastY = rect.y + rect.height; // SubSampledRead readSubSampled(rect, originalBounds, lastX, lastY, xSubsamplingFactor, ySubsamplingFactor, rasterData, buffer, nComponents); goOn = compressor.Push_stripe(buffer.array(), stripeHeights, sampleOffsets, sampleGaps, rowGaps, precisions); y += stripeHeights[0]; buffer.clear(); rasterData = null; } } bufferValues = null; } } } /** * Set the number of decomposition levels * @param cLevels * @param destinationWidth * @return */ private int setDecompositionLevels(final int cLevels, final int destinationWidth) { final int power = POWERS_2[cLevels]; int levels = cLevels; if (((double) destinationWidth / power) < 20) { // // // // Try to automagically find a proper value // // // int i = 0; for (; i < POWERS_2.length; i++) { final double division = (double) destinationWidth / POWERS_2[i]; if (division < 20) { break; } } levels = i > 1 ? i - 1 : 1; } return levels; } /** * Compute the bytes overhead due to Markers. * @param orgGen_tlm * @param tileH * @param tileW * @param destinationHeight * @param destinationWidth * @param addComment */ private int addMarkerBytes( final int components, final int destinationWidth, final int destinationHeight, final int tileW, final int tileH, final int orgGen_tlm, final boolean addComment) { int bytesOverhead = 0; int siz_size = (38 + components * 3) + 2; bytesOverhead += MarkerSize.SOC + siz_size + MarkerSize.COD + MarkerSize.QCD_ESTIMATE + (addComment ? MarkerSize.COM : MarkerSize.COM_KAKADUV) + MarkerSize.SOT + MarkerSize.SOD + MarkerSize.EOC; if (tileW != Integer.MIN_VALUE && tileH != Integer.MIN_VALUE && tileW != 0 && tileH != 0 && orgGen_tlm != JP2KKakaduImageWriteParam.UNSPECIFIED_ORG_GEN_TLM){ final int nTileX = (int) Math.ceil((double)destinationWidth/tileW); final int nTileY = (int) Math.ceil((double)destinationHeight/tileH); final int overhead_TLM = nTileX * nTileY * 6; //"6" is a multiplier factor found after values analysis bytesOverhead += overhead_TLM + MarkerSize.TLM; } return bytesOverhead; } /** * Initialize the codestream params in compliance with the provided arguments. * * @param codeStream * the output codestream * @param ycc * the cycc parameter * @param cLevels * the number of DWT decomposition levels * @param quality * the quality factor (in the range 0.01 - 1. where 1 = LOSSLESS) * @param qualityLayers * the number of quality layers * @param colorModel * the input image's colorModel * @param writeCodeStreamOnly * <code>true</code> if we need to write only codestream. * @param target * the optional {@link Jp2_target} object in case we are * writing only codestream. * @param orgGen_plt * if {@code true}, request the insertion of packet length information * in the header of tile-parts. * @param orgGen_tlm * the Tile-part-lenght (TLM) marker marker segments in the main header. * @param orgT_parts * parameter setting the division of each tile's packets into tile-parts * @param qGuard * parameter setting the guard bits * @param cOrder * the {@link ProgressionOrder} to be used. * @param cPrecincts * the optional Precincts settings * @return {@code false} in case we aren't writing codestream only * and a proper {@link Jp2_target} haven't been provided * * @throws KduException */ private boolean initializeCodestream(final Kdu_codestream codeStream, final boolean ycc, final int cLevels, final double quality, final int qualityLayers, final ColorModel colorModel, final boolean writeCodeStreamOnly, final int dataType, final Jp2_target target, final byte[] geoJp2, final boolean orgGen_plt, final int orgGen_tlm, final String orgT_parts, final int qGuard, final ProgressionOrder cOrder, final String cPrecincts, final Compression compression) throws KduException { final Siz_params params = codeStream.Access_siz(); // // // // Setting SIZ_params // // // if (compression != null && compression == Compression.LOSSY){ params.Parse_string("Creversible=no"); } else if (quality == 1 || colorModel instanceof IndexColorModel) { params.Parse_string("Creversible=yes"); } else { params.Parse_string("Creversible=no"); } if (cPrecincts != null && cPrecincts.trim().length() > 0){ params.Parse_string("Cuse_precincts=yes"); params.Parse_string("Cprecincts="+cPrecincts); } params.Parse_string("Cycc=" + (ycc ? "yes" : "no")); params.Parse_string("Clevels=" + cLevels); params.Parse_string("Clayers=" + qualityLayers); if (dataType == DataBuffer.TYPE_SHORT || dataType == DataBuffer.TYPE_USHORT ){ params.Parse_string("Qstep=0.0000152588"); } if (qGuard > 0) { params.Parse_string("Qguard=" + qGuard); } // // // // Setting ORG_params // // // Kdu_params org = params.Access_cluster(Kdu_global.ORG_params); if (orgGen_plt){ org.Set("ORGgen_plt",0,0,true); } if (orgGen_tlm != JP2KKakaduImageWriteParam.UNSPECIFIED_ORG_GEN_TLM){ org.Set("ORGgen_tlm",0,0,orgGen_tlm); } if (orgT_parts != null && orgT_parts.length() > 0){ org.Parse_string("ORGt_parts=" + orgT_parts); } // // // // Setting COD_params // // // Kdu_params cod = params.Access_cluster(Kdu_global.COD_params); if (cOrder != null){ cod.Set("Corder", 0, 0, cOrder.getValue()); } // // // // Finalizing params // // // params.Finalize_all(); if (!writeCodeStreamOnly) { if (target == null) { return false; } // // // // Writing header // // // initializeHeader(target, params, colorModel); if (geoJp2 != null && geoJp2.length > 0){ //Add geoJP2 box here. //This is a quick solution. The ideal would be to leverage on metadata or on top of a better //set of entities/helpers target.Open_next(UUIDBox.BOX_TYPE); byte[] outByte = new byte[GEOJP2_UUID.length]; for (int i = 0; i < GEOJP2_UUID.length; i++){ outByte[i] = (byte) GEOJP2_UUID[i]; } target.Write(outByte, GEOJP2_UUID.length); target.Write(geoJp2, geoJp2.length); target.Close(); } target.Open_codestream(); } return true; } /** * Initialize the provided stripe compressor leveraging on a set of input parameters. * * @param compressor * the {@link Kdu_stripe_compressor} to be initialized. * @param codeStream * the {@link Kdu_codestream} * @param quality * the requested quality factor * @param qualityLayers * the number of specified quality layers * @param qualityLayersSize * the requested quality layers total size. * @param bitRates * @param compression * @param rowSize * the size of a destination image row in pixels * @param destinationHeight * the destination image height * @param destinationWidth * the destination image width * @param nComponents * the number of components * @param addCommentMarker * @param stripeHeights * the stripeHeights array * @param sampleGaps * the sampleGaps array * @param rowGaps * the rowGaps array * @param sampleOffsets * the sampleOffsets array * @param precisions * the precisions array * @param numberOfBits * the bit depth array * @throws KduException */ private void initializeStripeCompressor( final Kdu_stripe_compressor compressor, final Kdu_codestream codeStream, final double quality, final int qualityLayers, final long qualityLayersSize, final double[] bitRates, final Compression compression, final int rowSize, final int destinationHeight, final int destinationWidth, final int nComponents, final int[] stripeHeights, final int[] sampleGaps, final int[] rowGaps, final int[] sampleOffsets, final int[] precisions, final int[] numberOfBits, final boolean addCommentMarker) throws KduException { final long[] cumulativeQualityLayerSizes; switch (compression){ case LOSSY: cumulativeQualityLayerSizes = computeQualityLayers(qualityLayers, qualityLayersSize, bitRates); break; case UNDEFINED: case NUMERICALLY_LOSSLESS: default: if (KakaduUtilities.notEqual(quality, 1d) || bitRates != null) { cumulativeQualityLayerSizes = computeQualityLayers(qualityLayers, qualityLayersSize, bitRates); } else { cumulativeQualityLayerSizes = null; } break; } int maxStripeHeight = MAX_BUFFER_SIZE / (rowSize); if (maxStripeHeight > destinationHeight) { maxStripeHeight = destinationHeight; } else { // In case the computed stripeHeight is near to the destination height, // I will avoid multiple calls by doing a single push. double ratio = (double) maxStripeHeight / (double) destinationHeight; if (ratio > SINGLE_PUSH_THRESHOLD_RATIO) { maxStripeHeight = destinationHeight; } } int minStripeHeight = MIN_BUFFER_SIZE / (rowSize); if (minStripeHeight < 1) { minStripeHeight = 1; } else if (minStripeHeight > destinationHeight) { minStripeHeight = destinationHeight; } // // // // Filling arrays which will be used by push method. // Note that the arrays have been previously created before calling this method. // // // for (int component = 0; component < nComponents; component++) { stripeHeights[component] = maxStripeHeight; sampleGaps[component] = nComponents; rowGaps[component] = destinationWidth * nComponents; sampleOffsets[component] = component; precisions[component] = numberOfBits[component]; } // //////////////////////////////////////////////////////////////// // // Initializing Stripe Compressor // // //////////////////////////////////////////////////////////////// compressor.Start(codeStream, qualityLayers, cumulativeQualityLayerSizes, null, 0, false, false, addCommentMarker, 0, nComponents, false); final boolean useRecommendations = compressor.Get_recommended_stripe_heights( minStripeHeight, maxStripeHeight, stripeHeights, null); if (!useRecommendations) { // Setting the stripeHeight to the max affordable stripe height for (int i = 0; i < nComponents; i++) { stripeHeights[i] = maxStripeHeight; } } } /** * Given a requested number of qualityLayers, computes the cumulative * quality layers sizes to be set as argument of the stripe_compressor. * * @param qualityLayers * the number of quality layers * @param referenceSize * the total amount of bytes used by the quality layers. * @param bitRates * @return a <code>long</code> array containing the cumulative layers sizes. */ private long[] computeQualityLayers(final int qualityLayers, final long referenceSize, final double[] bitRates) { long[] cumulativeQualityLayerSizes = new long[qualityLayers]; if (bitRates != null){ // Use explicit bitRates provided. for (int i = 0; i < qualityLayers; i++) { cumulativeQualityLayerSizes[i] = (long) Math.floor(bitRates[i] * KakaduUtilities.BIT_TO_BYTE_FACTOR * (double)referenceSize); } } else if (qualityLayers > 1) { // sub-divide the total amount of bytes in the number of requested // quality layers. We use a dicotomic paradigm. The bytes assigned // to the N-th layer (Better quality) are 2x the bytes assigned // to the (N-1)-th layer. final long[] qualityLayerSizes = new long[qualityLayers]; final int[] multipliers = new int[qualityLayers]; int multi = 1; int totals = 0; // Computing the minimum quality layers size as well as the // total number of subdivisions. for (int i = 0; i < qualityLayers; i++) { multi = i != 0 ? multi * 2 : multi; totals += multi; multipliers[i] = multi; } double qualityStep = Math.floor((double) referenceSize) / ((double) totals); // Setting the cumulative layers sizes. for (int i = 0; i < qualityLayers - 1; i++) { long step = i != 0 ? qualityLayerSizes[i - 1] : 0; qualityLayerSizes[i] = (long) Math.floor(qualityStep * multipliers[i]); cumulativeQualityLayerSizes[i] = qualityLayerSizes[i] + step; } cumulativeQualityLayerSizes[qualityLayers - 1] = referenceSize; } else { // Use a single quality layer. cumulativeQualityLayerSizes[0] = referenceSize; } return cumulativeQualityLayerSizes; } /** * Set jp2 elements to be properly written in the JP2 Header. * * @param target * the {@link Jp2_target} object. * @param sizParams * the {@link Siz_params} containing information needed for * initialization. * @param colorModel * the color model of the image to be written. * @throws KduException */ private void initializeHeader(final Jp2_target target, final Siz_params sizParams, final ColorModel colorModel) throws KduException { // // // // Init the jp2 dimensions // // // final Jp2_dimensions dims = target.Access_dimensions(); dims.Init(sizParams); // // // // Init the jp2 colour // // // final Jp2_colour colour = target.Access_colour(); final int cs = colorModel.getColorSpace().getType(); if (cs == ColorSpace.TYPE_RGB) { colour.Init(kdu_jni.Kdu_global.JP2_sRGB_SPACE); } else if (cs == ColorSpace.TYPE_GRAY) { colour.Init(kdu_jni.Kdu_global.JP2_sLUM_SPACE); } // // // // In case the input image has a palette, initialize jp2 palette and channels. // // // if (colorModel instanceof IndexColorModel) { final IndexColorModel icm = (IndexColorModel) colorModel; final int bitDepth = icm.getComponentSize(0); final int lutSize = icm.getMapSize(); final int reds[] = new int[lutSize]; final int blues[] = new int[lutSize]; final int greens[] = new int[lutSize]; // Getting the palette entries for (int i = 0; i < lutSize; i++) { reds[i] = icm.getRed(i); greens[i] = icm.getGreen(i); blues[i] = icm.getBlue(i); } // Setting the look up table for the jp2 output. Jp2_palette palette = target.Access_palette(); palette.Init(3, lutSize); palette.Set_lut(0, reds, bitDepth, false); palette.Set_lut(1, greens, bitDepth, false); palette.Set_lut(2, blues, bitDepth, false); // Setting channels Jp2_channels channels = target.Access_channels(); channels.Init(3); channels.Set_colour_mapping(0, 0, 0); channels.Set_colour_mapping(1, 0, 1); channels.Set_colour_mapping(2, 0, 2); } // Finish the initialization by writing the header target.Write_header(); } /** * Read a region of an input raster and put the data values in the provided * Buffer. * * @param region * the requested region * @param originalBounds * the original Image Extent * @param lastX * the last pixel to be analyzed along the image width. * @param lastY * the last pixel to be analyzed along the image height. * @param xSubsamplingFactor * the subsampling factor along the image width. * @param ySubsamplingFactor * the subsampling factor along the image height. * @param rasterData * the original raster data. * @param dataBuffer * the buffer containing the data read. * @param nComponents * the number of image components. */ private void readSubSampled(final Rectangle region, final Rectangle originalBounds, final int lastX, final int lastY, final int xSubsamplingFactor, final int ySubsamplingFactor, final Raster rasterData, final Buffer dataBuffer, final int nComponents) { if (dataBuffer instanceof ByteBuffer) { // // // // Byte read // // // final byte[] data = new byte[nComponents]; final ByteBuffer buffer = (ByteBuffer) dataBuffer; for (int j = region.y; j < lastY; j++) { if (((j - originalBounds.y) % ySubsamplingFactor) != 0) { continue; } for (int i = region.x; i < lastX; i++) { if (((i - originalBounds.x) % xSubsamplingFactor) != 0) { continue; } rasterData.getDataElements(i, j, data); buffer.put(data, 0, nComponents); } } } else if (dataBuffer instanceof ShortBuffer) { // // // // Short read // // // final short[] data = new short[nComponents]; final ShortBuffer buffer = (ShortBuffer) dataBuffer; for (int j = region.y; j < lastY; j++) { if (((j - originalBounds.y) % ySubsamplingFactor) != 0) { continue; } for (int i = region.x; i < lastX; i++) { if (((i - originalBounds.x) % xSubsamplingFactor) != 0) { continue; } rasterData.getDataElements(i, j, data); buffer.put(data, 0, nComponents); } } } else if (dataBuffer instanceof IntBuffer) { // // // // Int read // // // final IntBuffer buffer = (IntBuffer) dataBuffer; final int[] data = new int[nComponents]; for (int j = region.y; j < lastY; j++) { if (((j - originalBounds.y) % ySubsamplingFactor) != 0) { continue; } for (int i = region.x; i < lastX; i++) { if (((i - originalBounds.x) % xSubsamplingFactor) != 0) { continue; } rasterData.getDataElements(i, j, data); buffer.put(data, 0, nComponents); } } } else { throw new IllegalArgumentException("Unsupported buffer type"); } } /** * Initialize the {@link Siz_params} object with the basic set of provided information, * such as image size, number of components, bit depth, data sign and tiling. * * @param params * the {@link Siz_params} to be set. * @param width * the image width * @param height * the image height * @param precision * the number of bits of each component. * @param components * the number of components * @param isSigned * {@code true} if data is signed. * @param tileW * the requested inner tiling width * @param tileH * the requested inner tiling height * @throws KduException */ private void initializeParams(Siz_params params, final int width, final int height, final int precision, final int components, final boolean isSigned, final int tileW, final int tileH, final int sProfile) throws KduException { params.Set("Ssize", 0, 0, height); params.Set("Ssize", 0, 1, width); params.Set("Sprofile", 0, 0, sProfile); params.Set("Sorigin", 0, 0, 0); params.Set("Sorigin", 0, 1, 0); params.Set("Scomponents", 0, 0, components); params.Set("Sprecision", 0, 0, precision); params.Set("Sdims", 0, 0, height); params.Set("Sdims", 0, 1, width); params.Set("Ssigned", 0, 0, isSigned); if (tileH != Integer.MIN_VALUE && tileW != Integer.MIN_VALUE){ params.Set("Stiles", 0, 0, tileH); params.Set("Stiles", 0, 1, tileW); } params.Finalize(); } }