| /* |
| * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code 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 General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package com.sun.imageio.plugins.jpeg; |
| |
| import javax.imageio.IIOException; |
| import javax.imageio.ImageReader; |
| import javax.imageio.ImageReadParam; |
| import javax.imageio.ImageTypeSpecifier; |
| import javax.imageio.metadata.IIOMetadata; |
| import javax.imageio.spi.ImageReaderSpi; |
| import javax.imageio.stream.ImageInputStream; |
| import javax.imageio.plugins.jpeg.JPEGImageReadParam; |
| import javax.imageio.plugins.jpeg.JPEGQTable; |
| import javax.imageio.plugins.jpeg.JPEGHuffmanTable; |
| |
| import java.awt.Point; |
| import java.awt.Rectangle; |
| import java.awt.color.ColorSpace; |
| import java.awt.color.ICC_Profile; |
| import java.awt.color.ICC_ColorSpace; |
| import java.awt.color.CMMException; |
| import java.awt.image.BufferedImage; |
| import java.awt.image.Raster; |
| import java.awt.image.WritableRaster; |
| import java.awt.image.DataBuffer; |
| import java.awt.image.DataBufferByte; |
| import java.awt.image.ColorModel; |
| import java.awt.image.IndexColorModel; |
| import java.awt.image.ColorConvertOp; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.Iterator; |
| import java.util.ArrayList; |
| import java.util.NoSuchElementException; |
| |
| import sun.java2d.Disposer; |
| import sun.java2d.DisposerRecord; |
| |
| public class JPEGImageReader extends ImageReader { |
| |
| private boolean debug = false; |
| |
| /** |
| * The following variable contains a pointer to the IJG library |
| * structure for this reader. It is assigned in the constructor |
| * and then is passed in to every native call. It is set to 0 |
| * by dispose to avoid disposing twice. |
| */ |
| private long structPointer = 0; |
| |
| /** The input stream we read from */ |
| private ImageInputStream iis = null; |
| |
| /** |
| * List of stream positions for images, reinitialized every time |
| * a new input source is set. |
| */ |
| private List<Long> imagePositions = null; |
| |
| /** |
| * The number of images in the stream, or 0. |
| */ |
| private int numImages = 0; |
| |
| static { |
| java.security.AccessController.doPrivileged( |
| new java.security.PrivilegedAction<Void>() { |
| public Void run() { |
| System.loadLibrary("javajpeg"); |
| return null; |
| } |
| }); |
| initReaderIDs(ImageInputStream.class, |
| JPEGQTable.class, |
| JPEGHuffmanTable.class); |
| } |
| |
| // The following warnings are converted to strings when used |
| // as keys to get localized resources from JPEGImageReaderResources |
| // and its children. |
| |
| /** |
| * Warning code to be passed to warningOccurred to indicate |
| * that the EOI marker is missing from the end of the stream. |
| * This usually signals that the stream is corrupted, but |
| * everything up to the last MCU should be usable. |
| */ |
| protected static final int WARNING_NO_EOI = 0; |
| |
| /** |
| * Warning code to be passed to warningOccurred to indicate |
| * that a JFIF segment was encountered inside a JFXX JPEG |
| * thumbnail and is being ignored. |
| */ |
| protected static final int WARNING_NO_JFIF_IN_THUMB = 1; |
| |
| /** |
| * Warning code to be passed to warningOccurred to indicate |
| * that embedded ICC profile is invalid and will be ignored. |
| */ |
| protected static final int WARNING_IGNORE_INVALID_ICC = 2; |
| |
| private static final int MAX_WARNING = WARNING_IGNORE_INVALID_ICC; |
| |
| /** |
| * Image index of image for which header information |
| * is available. |
| */ |
| private int currentImage = -1; |
| |
| // The following is copied out from C after reading the header. |
| // Unlike metadata, which may never be retrieved, we need this |
| // if we are to read an image at all. |
| |
| /** Set by setImageData native code callback */ |
| private int width; |
| /** Set by setImageData native code callback */ |
| private int height; |
| /** |
| * Set by setImageData native code callback. A modified |
| * IJG+NIFTY colorspace code. |
| */ |
| private int colorSpaceCode; |
| /** |
| * Set by setImageData native code callback. A modified |
| * IJG+NIFTY colorspace code. |
| */ |
| private int outColorSpaceCode; |
| /** Set by setImageData native code callback */ |
| private int numComponents; |
| /** Set by setImageData native code callback */ |
| private ColorSpace iccCS = null; |
| |
| |
| /** If we need to post-convert in Java, convert with this op */ |
| private ColorConvertOp convert = null; |
| |
| /** The image we are going to fill */ |
| private BufferedImage image = null; |
| |
| /** An intermediate Raster to hold decoded data */ |
| private WritableRaster raster = null; |
| |
| /** A view of our target Raster that we can setRect to */ |
| private WritableRaster target = null; |
| |
| /** The databuffer for the above Raster */ |
| private DataBufferByte buffer = null; |
| |
| /** The region in the destination where we will write pixels */ |
| private Rectangle destROI = null; |
| |
| /** The list of destination bands, if any */ |
| private int [] destinationBands = null; |
| |
| /** Stream metadata, cached, even when the stream is changed. */ |
| private JPEGMetadata streamMetadata = null; |
| |
| /** Image metadata, valid for the imageMetadataIndex only. */ |
| private JPEGMetadata imageMetadata = null; |
| private int imageMetadataIndex = -1; |
| |
| /** |
| * Set to true every time we seek in the stream; used to |
| * invalidate the native buffer contents in C. |
| */ |
| private boolean haveSeeked = false; |
| |
| /** |
| * Tables that have been read from a tables-only image at the |
| * beginning of a stream. |
| */ |
| private JPEGQTable [] abbrevQTables = null; |
| private JPEGHuffmanTable[] abbrevDCHuffmanTables = null; |
| private JPEGHuffmanTable[] abbrevACHuffmanTables = null; |
| |
| private int minProgressivePass = 0; |
| private int maxProgressivePass = Integer.MAX_VALUE; |
| |
| /** |
| * Variables used by progress monitoring. |
| */ |
| private static final int UNKNOWN = -1; // Number of passes |
| private static final int MIN_ESTIMATED_PASSES = 10; // IJG default |
| private int knownPassCount = UNKNOWN; |
| private int pass = 0; |
| private float percentToDate = 0.0F; |
| private float previousPassPercentage = 0.0F; |
| private int progInterval = 0; |
| |
| /** |
| * Set to true once stream has been checked for stream metadata |
| */ |
| private boolean tablesOnlyChecked = false; |
| |
| /** The referent to be registered with the Disposer. */ |
| private Object disposerReferent = new Object(); |
| |
| /** The DisposerRecord that handles the actual disposal of this reader. */ |
| private DisposerRecord disposerRecord; |
| |
| /** Sets up static C structures. */ |
| private static native void initReaderIDs(Class<?> iisClass, |
| Class<?> qTableClass, |
| Class<?> huffClass); |
| |
| public JPEGImageReader(ImageReaderSpi originator) { |
| super(originator); |
| structPointer = initJPEGImageReader(); |
| disposerRecord = new JPEGReaderDisposerRecord(structPointer); |
| Disposer.addRecord(disposerReferent, disposerRecord); |
| } |
| |
| /** Sets up per-reader C structure and returns a pointer to it. */ |
| private native long initJPEGImageReader(); |
| |
| /** |
| * Called by the native code or other classes to signal a warning. |
| * The code is used to lookup a localized message to be used when |
| * sending warnings to listeners. |
| */ |
| protected void warningOccurred(int code) { |
| cbLock.lock(); |
| try { |
| if ((code < 0) || (code > MAX_WARNING)){ |
| throw new InternalError("Invalid warning index"); |
| } |
| processWarningOccurred |
| ("com.sun.imageio.plugins.jpeg.JPEGImageReaderResources", |
| Integer.toString(code)); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| /** |
| * The library has it's own error facility that emits warning messages. |
| * This routine is called by the native code when it has already |
| * formatted a string for output. |
| * XXX For truly complete localization of all warning messages, |
| * the sun_jpeg_output_message routine in the native code should |
| * send only the codes and parameters to a method here in Java, |
| * which will then format and send the warnings, using localized |
| * strings. This method will have to deal with all the parameters |
| * and formats (%u with possibly large numbers, %02d, %02x, etc.) |
| * that actually occur in the JPEG library. For now, this prevents |
| * library warnings from being printed to stderr. |
| */ |
| protected void warningWithMessage(String msg) { |
| cbLock.lock(); |
| try { |
| processWarningOccurred(msg); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| public void setInput(Object input, |
| boolean seekForwardOnly, |
| boolean ignoreMetadata) |
| { |
| setThreadLock(); |
| try { |
| cbLock.check(); |
| |
| super.setInput(input, seekForwardOnly, ignoreMetadata); |
| this.ignoreMetadata = ignoreMetadata; |
| resetInternalState(); |
| iis = (ImageInputStream) input; // Always works |
| setSource(structPointer); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| /** |
| * This method is called from native code in order to fill |
| * native input buffer. |
| * |
| * We block any attempt to change the reading state during this |
| * method, in order to prevent a corruption of the native decoder |
| * state. |
| * |
| * @return number of bytes read from the stream. |
| */ |
| private int readInputData(byte[] buf, int off, int len) throws IOException { |
| cbLock.lock(); |
| try { |
| return iis.read(buf, off, len); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| /** |
| * This method is called from the native code in order to |
| * skip requested number of bytes in the input stream. |
| * |
| * @param n |
| * @return |
| * @throws IOException |
| */ |
| private long skipInputBytes(long n) throws IOException { |
| cbLock.lock(); |
| try { |
| return iis.skipBytes(n); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| private native void setSource(long structPointer); |
| |
| private void checkTablesOnly() throws IOException { |
| if (debug) { |
| System.out.println("Checking for tables-only image"); |
| } |
| long savePos = iis.getStreamPosition(); |
| if (debug) { |
| System.out.println("saved pos is " + savePos); |
| System.out.println("length is " + iis.length()); |
| } |
| // Read the first header |
| boolean tablesOnly = readNativeHeader(true); |
| if (tablesOnly) { |
| if (debug) { |
| System.out.println("tables-only image found"); |
| long pos = iis.getStreamPosition(); |
| System.out.println("pos after return from native is " + pos); |
| } |
| // This reads the tables-only image twice, once from C |
| // and once from Java, but only if ignoreMetadata is false |
| if (ignoreMetadata == false) { |
| iis.seek(savePos); |
| haveSeeked = true; |
| streamMetadata = new JPEGMetadata(true, false, |
| iis, this); |
| long pos = iis.getStreamPosition(); |
| if (debug) { |
| System.out.println |
| ("pos after constructing stream metadata is " + pos); |
| } |
| } |
| // Now we are at the first image if there are any, so add it |
| // to the list |
| if (hasNextImage()) { |
| imagePositions.add(iis.getStreamPosition()); |
| } |
| } else { // Not tables only, so add original pos to the list |
| imagePositions.add(savePos); |
| // And set current image since we've read it now |
| currentImage = 0; |
| } |
| if (seekForwardOnly) { |
| Long pos = imagePositions.get(imagePositions.size()-1); |
| iis.flushBefore(pos.longValue()); |
| } |
| tablesOnlyChecked = true; |
| } |
| |
| public int getNumImages(boolean allowSearch) throws IOException { |
| setThreadLock(); |
| try { // locked thread |
| cbLock.check(); |
| |
| return getNumImagesOnThread(allowSearch); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| @SuppressWarnings("fallthrough") |
| private int getNumImagesOnThread(boolean allowSearch) |
| throws IOException { |
| if (numImages != 0) { |
| return numImages; |
| } |
| if (iis == null) { |
| throw new IllegalStateException("Input not set"); |
| } |
| if (allowSearch == true) { |
| if (seekForwardOnly) { |
| throw new IllegalStateException( |
| "seekForwardOnly and allowSearch can't both be true!"); |
| } |
| // Otherwise we have to read the entire stream |
| |
| if (!tablesOnlyChecked) { |
| checkTablesOnly(); |
| } |
| |
| iis.mark(); |
| |
| gotoImage(0); |
| |
| JPEGBuffer buffer = new JPEGBuffer(iis); |
| buffer.loadBuf(0); |
| |
| boolean done = false; |
| while (!done) { |
| done = buffer.scanForFF(this); |
| switch (buffer.buf[buffer.bufPtr] & 0xff) { |
| case JPEG.SOI: |
| numImages++; |
| // FALL THROUGH to decrement buffer vars |
| // This first set doesn't have a length |
| case 0: // not a marker, just a data 0xff |
| case JPEG.RST0: |
| case JPEG.RST1: |
| case JPEG.RST2: |
| case JPEG.RST3: |
| case JPEG.RST4: |
| case JPEG.RST5: |
| case JPEG.RST6: |
| case JPEG.RST7: |
| case JPEG.EOI: |
| buffer.bufAvail--; |
| buffer.bufPtr++; |
| break; |
| // All the others have a length |
| default: |
| buffer.bufAvail--; |
| buffer.bufPtr++; |
| buffer.loadBuf(2); |
| int length = ((buffer.buf[buffer.bufPtr++] & 0xff) << 8) | |
| (buffer.buf[buffer.bufPtr++] & 0xff); |
| buffer.bufAvail -= 2; |
| length -= 2; // length includes itself |
| buffer.skipData(length); |
| } |
| } |
| |
| |
| iis.reset(); |
| |
| return numImages; |
| } |
| |
| return -1; // Search is necessary for JPEG |
| } |
| |
| /** |
| * Sets the input stream to the start of the requested image. |
| * <pre> |
| * @exception IllegalStateException if the input source has not been |
| * set. |
| * @exception IndexOutOfBoundsException if the supplied index is |
| * out of bounds. |
| * </pre> |
| */ |
| private void gotoImage(int imageIndex) throws IOException { |
| if (iis == null) { |
| throw new IllegalStateException("Input not set"); |
| } |
| if (imageIndex < minIndex) { |
| throw new IndexOutOfBoundsException(); |
| } |
| if (!tablesOnlyChecked) { |
| checkTablesOnly(); |
| } |
| if (imageIndex < imagePositions.size()) { |
| iis.seek(imagePositions.get(imageIndex).longValue()); |
| } else { |
| // read to start of image, saving positions |
| // First seek to the last position we already have, and skip the |
| // entire image |
| Long pos = imagePositions.get(imagePositions.size()-1); |
| iis.seek(pos.longValue()); |
| skipImage(); |
| // Now add all intervening positions, skipping images |
| for (int index = imagePositions.size(); |
| index <= imageIndex; |
| index++) { |
| // Is there an image? |
| if (!hasNextImage()) { |
| throw new IndexOutOfBoundsException(); |
| } |
| pos = iis.getStreamPosition(); |
| imagePositions.add(pos); |
| if (seekForwardOnly) { |
| iis.flushBefore(pos.longValue()); |
| } |
| if (index < imageIndex) { |
| skipImage(); |
| } // Otherwise we are where we want to be |
| } |
| } |
| |
| if (seekForwardOnly) { |
| minIndex = imageIndex; |
| } |
| |
| haveSeeked = true; // No way is native buffer still valid |
| } |
| |
| /** |
| * Skip over a complete image in the stream, leaving the stream |
| * positioned such that the next byte to be read is the first |
| * byte of the next image. For JPEG, this means that we read |
| * until we encounter an EOI marker or until the end of the stream. |
| * We can find data same as EOI marker in some headers |
| * or comments, so we have to skip bytes related to these headers. |
| * If the stream ends before an EOI marker is encountered, |
| * an IndexOutOfBoundsException is thrown. |
| */ |
| private void skipImage() throws IOException { |
| if (debug) { |
| System.out.println("skipImage called"); |
| } |
| // verify if image starts with an SOI marker |
| int initialFF = iis.read(); |
| if (initialFF == 0xff) { |
| int soiMarker = iis.read(); |
| if (soiMarker != JPEG.SOI) { |
| throw new IOException("skipImage : Invalid image doesn't " |
| + "start with SOI marker"); |
| } |
| } else { |
| throw new IOException("skipImage : Invalid image doesn't start " |
| + "with 0xff"); |
| } |
| boolean foundFF = false; |
| String IOOBE = "skipImage : Reached EOF before we got EOI marker"; |
| int markerLength = 2; |
| for (int byteval = iis.read(); |
| byteval != -1; |
| byteval = iis.read()) { |
| |
| if (foundFF == true) { |
| switch (byteval) { |
| case JPEG.EOI: |
| if (debug) { |
| System.out.println("skipImage : Found EOI at " + |
| (iis.getStreamPosition() - markerLength)); |
| } |
| return; |
| case JPEG.SOI: |
| throw new IOException("skipImage : Found extra SOI" |
| + " marker before getting to EOI"); |
| case 0: |
| case 255: |
| // markers which doesn't contain length data |
| case JPEG.RST0: |
| case JPEG.RST1: |
| case JPEG.RST2: |
| case JPEG.RST3: |
| case JPEG.RST4: |
| case JPEG.RST5: |
| case JPEG.RST6: |
| case JPEG.RST7: |
| case JPEG.TEM: |
| break; |
| // markers which contains length data |
| case JPEG.SOF0: |
| case JPEG.SOF1: |
| case JPEG.SOF2: |
| case JPEG.SOF3: |
| case JPEG.DHT: |
| case JPEG.SOF5: |
| case JPEG.SOF6: |
| case JPEG.SOF7: |
| case JPEG.JPG: |
| case JPEG.SOF9: |
| case JPEG.SOF10: |
| case JPEG.SOF11: |
| case JPEG.DAC: |
| case JPEG.SOF13: |
| case JPEG.SOF14: |
| case JPEG.SOF15: |
| case JPEG.SOS: |
| case JPEG.DQT: |
| case JPEG.DNL: |
| case JPEG.DRI: |
| case JPEG.DHP: |
| case JPEG.EXP: |
| case JPEG.APP0: |
| case JPEG.APP1: |
| case JPEG.APP2: |
| case JPEG.APP3: |
| case JPEG.APP4: |
| case JPEG.APP5: |
| case JPEG.APP6: |
| case JPEG.APP7: |
| case JPEG.APP8: |
| case JPEG.APP9: |
| case JPEG.APP10: |
| case JPEG.APP11: |
| case JPEG.APP12: |
| case JPEG.APP13: |
| case JPEG.APP14: |
| case JPEG.APP15: |
| case JPEG.COM: |
| // read length of header from next 2 bytes |
| int lengthHigherBits, lengthLowerBits, length; |
| lengthHigherBits = iis.read(); |
| if (lengthHigherBits != (-1)) { |
| lengthLowerBits = iis.read(); |
| if (lengthLowerBits != (-1)) { |
| length = (lengthHigherBits << 8) | |
| lengthLowerBits; |
| // length contains already read 2 bytes |
| length -= 2; |
| } else { |
| throw new IndexOutOfBoundsException(IOOBE); |
| } |
| } else { |
| throw new IndexOutOfBoundsException(IOOBE); |
| } |
| // skip the length specified in marker |
| iis.skipBytes(length); |
| break; |
| case (-1): |
| throw new IndexOutOfBoundsException(IOOBE); |
| default: |
| throw new IOException("skipImage : Invalid marker " |
| + "starting with ff " |
| + Integer.toHexString(byteval)); |
| } |
| } |
| foundFF = (byteval == 0xff); |
| } |
| throw new IndexOutOfBoundsException(IOOBE); |
| } |
| |
| /** |
| * Returns {@code true} if there is an image beyond |
| * the current stream position. Does not disturb the |
| * stream position. |
| */ |
| private boolean hasNextImage() throws IOException { |
| if (debug) { |
| System.out.print("hasNextImage called; returning "); |
| } |
| iis.mark(); |
| boolean foundFF = false; |
| for (int byteval = iis.read(); |
| byteval != -1; |
| byteval = iis.read()) { |
| |
| if (foundFF == true) { |
| if (byteval == JPEG.SOI) { |
| iis.reset(); |
| if (debug) { |
| System.out.println("true"); |
| } |
| return true; |
| } |
| } |
| foundFF = (byteval == 0xff) ? true : false; |
| } |
| // We hit the end of the stream before we hit an SOI, so no image |
| iis.reset(); |
| if (debug) { |
| System.out.println("false"); |
| } |
| return false; |
| } |
| |
| /** |
| * Push back the given number of bytes to the input stream. |
| * Called by the native code at the end of each image so |
| * that the next one can be identified from Java. |
| */ |
| private void pushBack(int num) throws IOException { |
| if (debug) { |
| System.out.println("pushing back " + num + " bytes"); |
| } |
| cbLock.lock(); |
| try { |
| iis.seek(iis.getStreamPosition()-num); |
| // The buffer is clear after this, so no need to set haveSeeked. |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| /** |
| * Reads header information for the given image, if possible. |
| */ |
| private void readHeader(int imageIndex, boolean reset) |
| throws IOException { |
| gotoImage(imageIndex); |
| readNativeHeader(reset); // Ignore return |
| currentImage = imageIndex; |
| } |
| |
| private boolean readNativeHeader(boolean reset) throws IOException { |
| boolean retval = false; |
| retval = readImageHeader(structPointer, haveSeeked, reset); |
| haveSeeked = false; |
| return retval; |
| } |
| |
| /** |
| * Read in the header information starting from the current |
| * stream position, returning {@code true} if the |
| * header was a tables-only image. After this call, the |
| * native IJG decompression struct will contain the image |
| * information required by most query calls below |
| * (e.g. getWidth, getHeight, etc.), if the header was not |
| * a tables-only image. |
| * If reset is {@code true}, the state of the IJG |
| * object is reset so that it can read a header again. |
| * This happens automatically if the header was a tables-only |
| * image. |
| */ |
| private native boolean readImageHeader(long structPointer, |
| boolean clearBuffer, |
| boolean reset) |
| throws IOException; |
| |
| /* |
| * Called by the native code whenever an image header has been |
| * read. Whether we read metadata or not, we always need this |
| * information, so it is passed back independently of |
| * metadata, which may never be read. |
| */ |
| private void setImageData(int width, |
| int height, |
| int colorSpaceCode, |
| int outColorSpaceCode, |
| int numComponents, |
| byte [] iccData) { |
| this.width = width; |
| this.height = height; |
| this.colorSpaceCode = colorSpaceCode; |
| this.outColorSpaceCode = outColorSpaceCode; |
| this.numComponents = numComponents; |
| |
| if (iccData == null) { |
| iccCS = null; |
| return; |
| } |
| |
| ICC_Profile newProfile = null; |
| try { |
| newProfile = ICC_Profile.getInstance(iccData); |
| } catch (IllegalArgumentException e) { |
| /* |
| * Color profile data seems to be invalid. |
| * Ignore this profile. |
| */ |
| iccCS = null; |
| warningOccurred(WARNING_IGNORE_INVALID_ICC); |
| |
| return; |
| } |
| byte[] newData = newProfile.getData(); |
| |
| ICC_Profile oldProfile = null; |
| if (iccCS instanceof ICC_ColorSpace) { |
| oldProfile = ((ICC_ColorSpace)iccCS).getProfile(); |
| } |
| byte[] oldData = null; |
| if (oldProfile != null) { |
| oldData = oldProfile.getData(); |
| } |
| |
| /* |
| * At the moment we can't rely on the ColorSpace.equals() |
| * and ICC_Profile.equals() because they do not detect |
| * the case when two profiles are created from same data. |
| * |
| * So, we have to do data comparison in order to avoid |
| * creation of different ColorSpace instances for the same |
| * embedded data. |
| */ |
| if (oldData == null || |
| !java.util.Arrays.equals(oldData, newData)) |
| { |
| iccCS = new ICC_ColorSpace(newProfile); |
| // verify new color space |
| try { |
| float[] colors = iccCS.fromRGB(new float[] {1f, 0f, 0f}); |
| } catch (CMMException e) { |
| /* |
| * Embedded profile seems to be corrupted. |
| * Ignore this profile. |
| */ |
| iccCS = null; |
| cbLock.lock(); |
| try { |
| warningOccurred(WARNING_IGNORE_INVALID_ICC); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| } |
| } |
| |
| public int getWidth(int imageIndex) throws IOException { |
| setThreadLock(); |
| try { |
| if (currentImage != imageIndex) { |
| cbLock.check(); |
| readHeader(imageIndex, true); |
| } |
| return width; |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public int getHeight(int imageIndex) throws IOException { |
| setThreadLock(); |
| try { |
| if (currentImage != imageIndex) { |
| cbLock.check(); |
| readHeader(imageIndex, true); |
| } |
| return height; |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| /////////// Color Conversion and Image Types |
| |
| /** |
| * Return an ImageTypeSpecifier corresponding to the given |
| * color space code, or null if the color space is unsupported. |
| */ |
| private ImageTypeProducer getImageType(int code) { |
| ImageTypeProducer ret = null; |
| |
| if ((code > 0) && (code < JPEG.NUM_JCS_CODES)) { |
| ret = ImageTypeProducer.getTypeProducer(code); |
| } |
| return ret; |
| } |
| |
| public ImageTypeSpecifier getRawImageType(int imageIndex) |
| throws IOException { |
| setThreadLock(); |
| try { |
| if (currentImage != imageIndex) { |
| cbLock.check(); |
| |
| readHeader(imageIndex, true); |
| } |
| |
| // Returns null if it can't be represented |
| return getImageType(colorSpaceCode).getType(); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) |
| throws IOException { |
| setThreadLock(); |
| try { |
| return getImageTypesOnThread(imageIndex); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private Iterator<ImageTypeSpecifier> getImageTypesOnThread(int imageIndex) |
| throws IOException { |
| if (currentImage != imageIndex) { |
| cbLock.check(); |
| readHeader(imageIndex, true); |
| } |
| |
| // We return an iterator containing the default, any |
| // conversions that the library provides, and |
| // all the other default types with the same number |
| // of components, as we can do these as a post-process. |
| // As we convert Rasters rather than images, images |
| // with alpha cannot be converted in a post-process. |
| |
| // If this image can't be interpreted, this method |
| // returns an empty Iterator. |
| |
| // Get the raw ITS, if there is one. Note that this |
| // won't always be the same as the default. |
| ImageTypeProducer raw = getImageType(colorSpaceCode); |
| |
| // Given the encoded colorspace, build a list of ITS's |
| // representing outputs you could handle starting |
| // with the default. |
| |
| ArrayList<ImageTypeProducer> list = new ArrayList<ImageTypeProducer>(1); |
| |
| switch (colorSpaceCode) { |
| case JPEG.JCS_GRAYSCALE: |
| list.add(raw); |
| list.add(getImageType(JPEG.JCS_RGB)); |
| break; |
| case JPEG.JCS_RGB: |
| list.add(raw); |
| list.add(getImageType(JPEG.JCS_GRAYSCALE)); |
| list.add(getImageType(JPEG.JCS_YCC)); |
| break; |
| case JPEG.JCS_RGBA: |
| list.add(raw); |
| break; |
| case JPEG.JCS_YCC: |
| if (raw != null) { // Might be null if PYCC.pf not installed |
| list.add(raw); |
| list.add(getImageType(JPEG.JCS_RGB)); |
| } |
| break; |
| case JPEG.JCS_YCCA: |
| if (raw != null) { // Might be null if PYCC.pf not installed |
| list.add(raw); |
| } |
| break; |
| case JPEG.JCS_YCbCr: |
| // As there is no YCbCr ColorSpace, we can't support |
| // the raw type. |
| |
| // due to 4705399, use RGB as default in order to avoid |
| // slowing down of drawing operations with result image. |
| list.add(getImageType(JPEG.JCS_RGB)); |
| |
| if (iccCS != null) { |
| list.add(new ImageTypeProducer() { |
| protected ImageTypeSpecifier produce() { |
| return ImageTypeSpecifier.createInterleaved |
| (iccCS, |
| JPEG.bOffsRGB, // Assume it's for RGB |
| DataBuffer.TYPE_BYTE, |
| false, |
| false); |
| } |
| }); |
| |
| } |
| |
| list.add(getImageType(JPEG.JCS_GRAYSCALE)); |
| list.add(getImageType(JPEG.JCS_YCC)); |
| break; |
| case JPEG.JCS_YCbCrA: // Default is to convert to RGBA |
| // As there is no YCbCr ColorSpace, we can't support |
| // the raw type. |
| list.add(getImageType(JPEG.JCS_RGBA)); |
| break; |
| } |
| |
| return new ImageTypeIterator(list.iterator()); |
| } |
| |
| /** |
| * Checks the implied color conversion between the stream and |
| * the target image, altering the IJG output color space if necessary. |
| * If a java color conversion is required, then this sets up |
| * {@code convert}. |
| * If bands are being rearranged at all (either source or destination |
| * bands are specified in the param), then the default color |
| * conversions are assumed to be correct. |
| * Throws an IIOException if there is no conversion available. |
| */ |
| private void checkColorConversion(BufferedImage image, |
| ImageReadParam param) |
| throws IIOException { |
| |
| // If we are rearranging channels at all, the default |
| // conversions remain in place. If the user wants |
| // raw channels then he should do this while reading |
| // a Raster. |
| if (param != null) { |
| if ((param.getSourceBands() != null) || |
| (param.getDestinationBands() != null)) { |
| // Accept default conversions out of decoder, silently |
| return; |
| } |
| } |
| |
| // XXX - We do not currently support any indexed color models, |
| // though we could, as IJG will quantize for us. |
| // This is a performance and memory-use issue, as |
| // users can read RGB and then convert to indexed in Java. |
| |
| ColorModel cm = image.getColorModel(); |
| |
| if (cm instanceof IndexColorModel) { |
| throw new IIOException("IndexColorModel not supported"); |
| } |
| |
| // Now check the ColorSpace type against outColorSpaceCode |
| // We may want to tweak the default |
| ColorSpace cs = cm.getColorSpace(); |
| int csType = cs.getType(); |
| convert = null; |
| switch (outColorSpaceCode) { |
| case JPEG.JCS_GRAYSCALE: // Its gray in the file |
| if (csType == ColorSpace.TYPE_RGB) { // We want RGB |
| // IJG can do this for us more efficiently |
| setOutColorSpace(structPointer, JPEG.JCS_RGB); |
| // Update java state according to changes |
| // in the native part of decoder. |
| outColorSpaceCode = JPEG.JCS_RGB; |
| numComponents = 3; |
| } else if (csType != ColorSpace.TYPE_GRAY) { |
| throw new IIOException("Incompatible color conversion"); |
| } |
| break; |
| case JPEG.JCS_RGB: // IJG wants to go to RGB |
| if (csType == ColorSpace.TYPE_GRAY) { // We want gray |
| if (colorSpaceCode == JPEG.JCS_YCbCr) { |
| // If the jpeg space is YCbCr, IJG can do it |
| setOutColorSpace(structPointer, JPEG.JCS_GRAYSCALE); |
| // Update java state according to changes |
| // in the native part of decoder. |
| outColorSpaceCode = JPEG.JCS_GRAYSCALE; |
| numComponents = 1; |
| } |
| } else if ((iccCS != null) && |
| (cm.getNumComponents() == numComponents) && |
| (cs != iccCS)) { |
| // We have an ICC profile but it isn't used in the dest |
| // image. So convert from the profile cs to the target cs |
| convert = new ColorConvertOp(iccCS, cs, null); |
| // Leave IJG conversion in place; we still need it |
| } else if ((iccCS == null) && |
| (!cs.isCS_sRGB()) && |
| (cm.getNumComponents() == numComponents)) { |
| // Target isn't sRGB, so convert from sRGB to the target |
| convert = new ColorConvertOp(JPEG.JCS.sRGB, cs, null); |
| } else if (csType != ColorSpace.TYPE_RGB) { |
| throw new IIOException("Incompatible color conversion"); |
| } |
| break; |
| case JPEG.JCS_RGBA: |
| // No conversions available; image must be RGBA |
| if ((csType != ColorSpace.TYPE_RGB) || |
| (cm.getNumComponents() != numComponents)) { |
| throw new IIOException("Incompatible color conversion"); |
| } |
| break; |
| case JPEG.JCS_YCC: |
| { |
| ColorSpace YCC = JPEG.JCS.getYCC(); |
| if (YCC == null) { // We can't do YCC at all |
| throw new IIOException("Incompatible color conversion"); |
| } |
| if ((cs != YCC) && |
| (cm.getNumComponents() == numComponents)) { |
| convert = new ColorConvertOp(YCC, cs, null); |
| } |
| } |
| break; |
| case JPEG.JCS_YCCA: |
| { |
| ColorSpace YCC = JPEG.JCS.getYCC(); |
| // No conversions available; image must be YCCA |
| if ((YCC == null) || // We can't do YCC at all |
| (cs != YCC) || |
| (cm.getNumComponents() != numComponents)) { |
| throw new IIOException("Incompatible color conversion"); |
| } |
| } |
| break; |
| default: |
| // Anything else we can't handle at all |
| throw new IIOException("Incompatible color conversion"); |
| } |
| } |
| |
| /** |
| * Set the IJG output space to the given value. The library will |
| * perform the appropriate colorspace conversions. |
| */ |
| private native void setOutColorSpace(long structPointer, int id); |
| |
| /////// End of Color Conversion & Image Types |
| |
| public ImageReadParam getDefaultReadParam() { |
| return new JPEGImageReadParam(); |
| } |
| |
| public IIOMetadata getStreamMetadata() throws IOException { |
| setThreadLock(); |
| try { |
| if (!tablesOnlyChecked) { |
| cbLock.check(); |
| checkTablesOnly(); |
| } |
| return streamMetadata; |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public IIOMetadata getImageMetadata(int imageIndex) |
| throws IOException { |
| setThreadLock(); |
| try { |
| // imageMetadataIndex will always be either a valid index or |
| // -1, in which case imageMetadata will not be null. |
| // So we can leave checking imageIndex for gotoImage. |
| if ((imageMetadataIndex == imageIndex) |
| && (imageMetadata != null)) { |
| return imageMetadata; |
| } |
| |
| cbLock.check(); |
| |
| gotoImage(imageIndex); |
| |
| imageMetadata = new JPEGMetadata(false, false, iis, this); |
| |
| imageMetadataIndex = imageIndex; |
| |
| return imageMetadata; |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public BufferedImage read(int imageIndex, ImageReadParam param) |
| throws IOException { |
| setThreadLock(); |
| try { |
| cbLock.check(); |
| try { |
| readInternal(imageIndex, param, false); |
| } catch (RuntimeException e) { |
| resetLibraryState(structPointer); |
| throw e; |
| } catch (IOException e) { |
| resetLibraryState(structPointer); |
| throw e; |
| } |
| |
| BufferedImage ret = image; |
| image = null; // don't keep a reference here |
| return ret; |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private Raster readInternal(int imageIndex, |
| ImageReadParam param, |
| boolean wantRaster) throws IOException { |
| readHeader(imageIndex, false); |
| |
| WritableRaster imRas = null; |
| int numImageBands = 0; |
| |
| if (!wantRaster){ |
| // Can we read this image? |
| Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex); |
| if (imageTypes.hasNext() == false) { |
| throw new IIOException("Unsupported Image Type"); |
| } |
| |
| image = getDestination(param, imageTypes, width, height); |
| imRas = image.getRaster(); |
| |
| // The destination may still be incompatible. |
| |
| numImageBands = image.getSampleModel().getNumBands(); |
| |
| // Check whether we can handle any implied color conversion |
| |
| // Throws IIOException if the stream and the image are |
| // incompatible, and sets convert if a java conversion |
| // is necessary |
| checkColorConversion(image, param); |
| |
| // Check the source and destination bands in the param |
| checkReadParamBandSettings(param, numComponents, numImageBands); |
| } else { |
| // Set the output color space equal to the input colorspace |
| // This disables all conversions |
| setOutColorSpace(structPointer, colorSpaceCode); |
| image = null; |
| } |
| |
| // Create an intermediate 1-line Raster that will hold the decoded, |
| // subsampled, clipped, band-selected image data in a single |
| // byte-interleaved buffer. The above transformations |
| // will occur in C for performance. Every time this Raster |
| // is filled we will call back to acceptPixels below to copy |
| // this to whatever kind of buffer our image has. |
| |
| int [] srcBands = JPEG.bandOffsets[numComponents-1]; |
| int numRasterBands = (wantRaster ? numComponents : numImageBands); |
| destinationBands = null; |
| |
| Rectangle srcROI = new Rectangle(0, 0, 0, 0); |
| destROI = new Rectangle(0, 0, 0, 0); |
| computeRegions(param, width, height, image, srcROI, destROI); |
| |
| int periodX = 1; |
| int periodY = 1; |
| |
| minProgressivePass = 0; |
| maxProgressivePass = Integer.MAX_VALUE; |
| |
| if (param != null) { |
| periodX = param.getSourceXSubsampling(); |
| periodY = param.getSourceYSubsampling(); |
| |
| int[] sBands = param.getSourceBands(); |
| if (sBands != null) { |
| srcBands = sBands; |
| numRasterBands = srcBands.length; |
| } |
| if (!wantRaster) { // ignore dest bands for Raster |
| destinationBands = param.getDestinationBands(); |
| } |
| |
| minProgressivePass = param.getSourceMinProgressivePass(); |
| maxProgressivePass = param.getSourceMaxProgressivePass(); |
| |
| if (param instanceof JPEGImageReadParam) { |
| JPEGImageReadParam jparam = (JPEGImageReadParam) param; |
| if (jparam.areTablesSet()) { |
| abbrevQTables = jparam.getQTables(); |
| abbrevDCHuffmanTables = jparam.getDCHuffmanTables(); |
| abbrevACHuffmanTables = jparam.getACHuffmanTables(); |
| } |
| } |
| } |
| |
| int lineSize = destROI.width*numRasterBands; |
| |
| buffer = new DataBufferByte(lineSize); |
| |
| int [] bandOffs = JPEG.bandOffsets[numRasterBands-1]; |
| |
| raster = Raster.createInterleavedRaster(buffer, |
| destROI.width, 1, |
| lineSize, |
| numRasterBands, |
| bandOffs, |
| null); |
| |
| // Now that we have the Raster we'll decode to, get a view of the |
| // target Raster that will permit a simple setRect for each scanline |
| if (wantRaster) { |
| target = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, |
| destROI.width, |
| destROI.height, |
| lineSize, |
| numRasterBands, |
| bandOffs, |
| null); |
| } else { |
| target = imRas; |
| } |
| int [] bandSizes = target.getSampleModel().getSampleSize(); |
| for (int i = 0; i < bandSizes.length; i++) { |
| if (bandSizes[i] <= 0 || bandSizes[i] > 8) { |
| throw new IIOException("Illegal band size: should be 0 < size <= 8"); |
| } |
| } |
| |
| /* |
| * If the process is sequential, and we have restart markers, |
| * we could skip to the correct restart marker, if the library |
| * lets us. That's an optimization to investigate later. |
| */ |
| |
| // Check for update listeners (don't call back if none) |
| boolean callbackUpdates = ((updateListeners != null) |
| || (progressListeners != null)); |
| |
| // Set up progression data |
| initProgressData(); |
| // if we have a metadata object, we can count the scans |
| // and set knownPassCount |
| if (imageIndex == imageMetadataIndex) { // We have metadata |
| knownPassCount = 0; |
| for (Iterator<MarkerSegment> iter = |
| imageMetadata.markerSequence.iterator(); iter.hasNext();) { |
| if (iter.next() instanceof SOSMarkerSegment) { |
| knownPassCount++; |
| } |
| } |
| } |
| progInterval = Math.max((target.getHeight()-1) / 20, 1); |
| if (knownPassCount > 0) { |
| progInterval *= knownPassCount; |
| } else if (maxProgressivePass != Integer.MAX_VALUE) { |
| progInterval *= (maxProgressivePass - minProgressivePass + 1); |
| } |
| |
| if (debug) { |
| System.out.println("**** Read Data *****"); |
| System.out.println("numRasterBands is " + numRasterBands); |
| System.out.print("srcBands:"); |
| for (int i = 0; i<srcBands.length;i++) |
| System.out.print(" " + srcBands[i]); |
| System.out.println(); |
| System.out.println("destination bands is " + destinationBands); |
| if (destinationBands != null) { |
| for (int i = 0; i < destinationBands.length; i++) { |
| System.out.print(" " + destinationBands[i]); |
| } |
| System.out.println(); |
| } |
| System.out.println("sourceROI is " + srcROI); |
| System.out.println("destROI is " + destROI); |
| System.out.println("periodX is " + periodX); |
| System.out.println("periodY is " + periodY); |
| System.out.println("minProgressivePass is " + minProgressivePass); |
| System.out.println("maxProgressivePass is " + maxProgressivePass); |
| System.out.println("callbackUpdates is " + callbackUpdates); |
| } |
| |
| // Finally, we are ready to read |
| |
| processImageStarted(currentImage); |
| |
| boolean aborted = false; |
| |
| // Note that getData disables acceleration on buffer, but it is |
| // just a 1-line intermediate data transfer buffer that will not |
| // affect the acceleration of the resulting image. |
| aborted = readImage(structPointer, |
| buffer.getData(), |
| numRasterBands, |
| srcBands, |
| bandSizes, |
| srcROI.x, srcROI.y, |
| srcROI.width, srcROI.height, |
| periodX, periodY, |
| abbrevQTables, |
| abbrevDCHuffmanTables, |
| abbrevACHuffmanTables, |
| minProgressivePass, maxProgressivePass, |
| callbackUpdates); |
| |
| if (aborted) { |
| processReadAborted(); |
| } else { |
| processImageComplete(); |
| } |
| |
| return target; |
| |
| } |
| |
| /** |
| * This method is called back from C when the intermediate Raster |
| * is full. The parameter indicates the scanline in the target |
| * Raster to which the intermediate Raster should be copied. |
| * After the copy, we notify update listeners. |
| */ |
| private void acceptPixels(int y, boolean progressive) { |
| if (convert != null) { |
| convert.filter(raster, raster); |
| } |
| target.setRect(destROI.x, destROI.y + y, raster); |
| |
| cbLock.lock(); |
| try { |
| processImageUpdate(image, |
| destROI.x, destROI.y+y, |
| raster.getWidth(), 1, |
| 1, 1, |
| destinationBands); |
| if ((y > 0) && (y%progInterval == 0)) { |
| int height = target.getHeight()-1; |
| float percentOfPass = ((float)y)/height; |
| if (progressive) { |
| if (knownPassCount != UNKNOWN) { |
| processImageProgress((pass + percentOfPass)*100.0F |
| / knownPassCount); |
| } else if (maxProgressivePass != Integer.MAX_VALUE) { |
| // Use the range of allowed progressive passes |
| processImageProgress((pass + percentOfPass)*100.0F |
| / (maxProgressivePass - minProgressivePass + 1)); |
| } else { |
| // Assume there are a minimum of MIN_ESTIMATED_PASSES |
| // and that there is always one more pass |
| // Compute the percentage as the percentage at the end |
| // of the previous pass, plus the percentage of this |
| // pass scaled to be the percentage of the total remaining, |
| // assuming a minimum of MIN_ESTIMATED_PASSES passes and |
| // that there is always one more pass. This is monotonic |
| // and asymptotic to 1.0, which is what we need. |
| int remainingPasses = // including this one |
| Math.max(2, MIN_ESTIMATED_PASSES-pass); |
| int totalPasses = pass + remainingPasses-1; |
| progInterval = Math.max(height/20*totalPasses, |
| totalPasses); |
| if (y%progInterval == 0) { |
| percentToDate = previousPassPercentage + |
| (1.0F - previousPassPercentage) |
| * (percentOfPass)/remainingPasses; |
| if (debug) { |
| System.out.print("pass= " + pass); |
| System.out.print(", y= " + y); |
| System.out.print(", progInt= " + progInterval); |
| System.out.print(", % of pass: " + percentOfPass); |
| System.out.print(", rem. passes: " |
| + remainingPasses); |
| System.out.print(", prev%: " |
| + previousPassPercentage); |
| System.out.print(", %ToDate: " + percentToDate); |
| System.out.print(" "); |
| } |
| processImageProgress(percentToDate*100.0F); |
| } |
| } |
| } else { |
| processImageProgress(percentOfPass * 100.0F); |
| } |
| } |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| private void initProgressData() { |
| knownPassCount = UNKNOWN; |
| pass = 0; |
| percentToDate = 0.0F; |
| previousPassPercentage = 0.0F; |
| progInterval = 0; |
| } |
| |
| private void passStarted (int pass) { |
| cbLock.lock(); |
| try { |
| this.pass = pass; |
| previousPassPercentage = percentToDate; |
| processPassStarted(image, |
| pass, |
| minProgressivePass, |
| maxProgressivePass, |
| 0, 0, |
| 1,1, |
| destinationBands); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| private void passComplete () { |
| cbLock.lock(); |
| try { |
| processPassComplete(image); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| void thumbnailStarted(int thumbnailIndex) { |
| cbLock.lock(); |
| try { |
| processThumbnailStarted(currentImage, thumbnailIndex); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| // Provide access to protected superclass method |
| void thumbnailProgress(float percentageDone) { |
| cbLock.lock(); |
| try { |
| processThumbnailProgress(percentageDone); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| // Provide access to protected superclass method |
| void thumbnailComplete() { |
| cbLock.lock(); |
| try { |
| processThumbnailComplete(); |
| } finally { |
| cbLock.unlock(); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if the read was aborted. |
| */ |
| private native boolean readImage(long structPointer, |
| byte [] buffer, |
| int numRasterBands, |
| int [] srcBands, |
| int [] bandSizes, |
| int sourceXOffset, int sourceYOffset, |
| int sourceWidth, int sourceHeight, |
| int periodX, int periodY, |
| JPEGQTable [] abbrevQTables, |
| JPEGHuffmanTable [] abbrevDCHuffmanTables, |
| JPEGHuffmanTable [] abbrevACHuffmanTables, |
| int minProgressivePass, |
| int maxProgressivePass, |
| boolean wantUpdates); |
| |
| public void abort() { |
| setThreadLock(); |
| try { |
| /** |
| * NB: we do not check the call back lock here, |
| * we allow to abort the reader any time. |
| */ |
| |
| super.abort(); |
| abortRead(structPointer); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| /** Set the C level abort flag. Keep it atomic for thread safety. */ |
| private native void abortRead(long structPointer); |
| |
| /** Resets library state when an exception occurred during a read. */ |
| private native void resetLibraryState(long structPointer); |
| |
| public boolean canReadRaster() { |
| return true; |
| } |
| |
| public Raster readRaster(int imageIndex, ImageReadParam param) |
| throws IOException { |
| setThreadLock(); |
| Raster retval = null; |
| try { |
| cbLock.check(); |
| /* |
| * This could be further optimized by not resetting the dest. |
| * offset and creating a translated raster in readInternal() |
| * (see bug 4994702 for more info). |
| */ |
| |
| // For Rasters, destination offset is logical, not physical, so |
| // set it to 0 before calling computeRegions, so that the destination |
| // region is not clipped. |
| Point saveDestOffset = null; |
| if (param != null) { |
| saveDestOffset = param.getDestinationOffset(); |
| param.setDestinationOffset(new Point(0, 0)); |
| } |
| retval = readInternal(imageIndex, param, true); |
| // Apply the destination offset, if any, as a logical offset |
| if (saveDestOffset != null) { |
| target = target.createWritableTranslatedChild(saveDestOffset.x, |
| saveDestOffset.y); |
| } |
| } catch (RuntimeException e) { |
| resetLibraryState(structPointer); |
| throw e; |
| } catch (IOException e) { |
| resetLibraryState(structPointer); |
| throw e; |
| } finally { |
| clearThreadLock(); |
| } |
| return retval; |
| } |
| |
| public boolean readerSupportsThumbnails() { |
| return true; |
| } |
| |
| public int getNumThumbnails(int imageIndex) throws IOException { |
| setThreadLock(); |
| try { |
| cbLock.check(); |
| |
| getImageMetadata(imageIndex); // checks iis state for us |
| // Now check the jfif segments |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) imageMetadata.findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| int retval = 0; |
| if (jfif != null) { |
| retval = (jfif.thumb == null) ? 0 : 1; |
| retval += jfif.extSegments.size(); |
| } |
| return retval; |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public int getThumbnailWidth(int imageIndex, int thumbnailIndex) |
| throws IOException { |
| setThreadLock(); |
| try { |
| cbLock.check(); |
| |
| if ((thumbnailIndex < 0) |
| || (thumbnailIndex >= getNumThumbnails(imageIndex))) { |
| throw new IndexOutOfBoundsException("No such thumbnail"); |
| } |
| // Now we know that there is a jfif segment |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) imageMetadata.findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| return jfif.getThumbnailWidth(thumbnailIndex); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public int getThumbnailHeight(int imageIndex, int thumbnailIndex) |
| throws IOException { |
| setThreadLock(); |
| try { |
| cbLock.check(); |
| |
| if ((thumbnailIndex < 0) |
| || (thumbnailIndex >= getNumThumbnails(imageIndex))) { |
| throw new IndexOutOfBoundsException("No such thumbnail"); |
| } |
| // Now we know that there is a jfif segment |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) imageMetadata.findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| return jfif.getThumbnailHeight(thumbnailIndex); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public BufferedImage readThumbnail(int imageIndex, |
| int thumbnailIndex) |
| throws IOException { |
| setThreadLock(); |
| try { |
| cbLock.check(); |
| |
| if ((thumbnailIndex < 0) |
| || (thumbnailIndex >= getNumThumbnails(imageIndex))) { |
| throw new IndexOutOfBoundsException("No such thumbnail"); |
| } |
| // Now we know that there is a jfif segment and that iis is good |
| JFIFMarkerSegment jfif = |
| (JFIFMarkerSegment) imageMetadata.findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| return jfif.getThumbnail(iis, thumbnailIndex, this); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private void resetInternalState() { |
| // reset C structures |
| resetReader(structPointer); |
| |
| // reset local Java structures |
| numImages = 0; |
| imagePositions = new ArrayList<>(); |
| currentImage = -1; |
| image = null; |
| raster = null; |
| target = null; |
| buffer = null; |
| destROI = null; |
| destinationBands = null; |
| streamMetadata = null; |
| imageMetadata = null; |
| imageMetadataIndex = -1; |
| haveSeeked = false; |
| tablesOnlyChecked = false; |
| iccCS = null; |
| initProgressData(); |
| } |
| |
| public void reset() { |
| setThreadLock(); |
| try { |
| cbLock.check(); |
| super.reset(); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private native void resetReader(long structPointer); |
| |
| public void dispose() { |
| setThreadLock(); |
| try { |
| cbLock.check(); |
| |
| if (structPointer != 0) { |
| disposerRecord.dispose(); |
| structPointer = 0; |
| } |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private static native void disposeReader(long structPointer); |
| |
| private static class JPEGReaderDisposerRecord implements DisposerRecord { |
| private long pData; |
| |
| public JPEGReaderDisposerRecord(long pData) { |
| this.pData = pData; |
| } |
| |
| public synchronized void dispose() { |
| if (pData != 0) { |
| disposeReader(pData); |
| pData = 0; |
| } |
| } |
| } |
| |
| private Thread theThread = null; |
| private int theLockCount = 0; |
| |
| private synchronized void setThreadLock() { |
| Thread currThread = Thread.currentThread(); |
| if (theThread != null) { |
| if (theThread != currThread) { |
| // it looks like that this reader instance is used |
| // by multiple threads. |
| throw new IllegalStateException("Attempt to use instance of " + |
| this + " locked on thread " + |
| theThread + " from thread " + |
| currThread); |
| } else { |
| theLockCount ++; |
| } |
| } else { |
| theThread = currThread; |
| theLockCount = 1; |
| } |
| } |
| |
| private synchronized void clearThreadLock() { |
| Thread currThread = Thread.currentThread(); |
| if (theThread == null || theThread != currThread) { |
| throw new IllegalStateException("Attempt to clear thread lock " + |
| " form wrong thread." + |
| " Locked thread: " + theThread + |
| "; current thread: " + currThread); |
| } |
| theLockCount --; |
| if (theLockCount == 0) { |
| theThread = null; |
| } |
| } |
| |
| private CallBackLock cbLock = new CallBackLock(); |
| |
| private static class CallBackLock { |
| |
| private State lockState; |
| |
| CallBackLock() { |
| lockState = State.Unlocked; |
| } |
| |
| void check() { |
| if (lockState != State.Unlocked) { |
| throw new IllegalStateException("Access to the reader is not allowed"); |
| } |
| } |
| |
| private void lock() { |
| lockState = State.Locked; |
| } |
| |
| private void unlock() { |
| lockState = State.Unlocked; |
| } |
| |
| private static enum State { |
| Unlocked, |
| Locked |
| } |
| } |
| } |
| |
| /** |
| * An internal helper class that wraps producer's iterator |
| * and extracts specifier instances on demand. |
| */ |
| class ImageTypeIterator implements Iterator<ImageTypeSpecifier> { |
| private Iterator<ImageTypeProducer> producers; |
| private ImageTypeSpecifier theNext = null; |
| |
| public ImageTypeIterator(Iterator<ImageTypeProducer> producers) { |
| this.producers = producers; |
| } |
| |
| public boolean hasNext() { |
| if (theNext != null) { |
| return true; |
| } |
| if (!producers.hasNext()) { |
| return false; |
| } |
| do { |
| theNext = producers.next().getType(); |
| } while (theNext == null && producers.hasNext()); |
| |
| return (theNext != null); |
| } |
| |
| public ImageTypeSpecifier next() { |
| if (theNext != null || hasNext()) { |
| ImageTypeSpecifier t = theNext; |
| theNext = null; |
| return t; |
| } else { |
| throw new NoSuchElementException(); |
| } |
| } |
| |
| public void remove() { |
| producers.remove(); |
| } |
| } |
| |
| /** |
| * An internal helper class that provides means for deferred creation |
| * of ImageTypeSpecifier instance required to describe available |
| * destination types. |
| * |
| * This implementation only supports standard |
| * jpeg color spaces (defined by corresponding JCS color space code). |
| * |
| * To support other color spaces one can override produce() method to |
| * return custom instance of ImageTypeSpecifier. |
| */ |
| class ImageTypeProducer { |
| |
| private ImageTypeSpecifier type = null; |
| boolean failed = false; |
| private int csCode; |
| |
| public ImageTypeProducer(int csCode) { |
| this.csCode = csCode; |
| } |
| |
| public ImageTypeProducer() { |
| csCode = -1; // undefined |
| } |
| |
| public synchronized ImageTypeSpecifier getType() { |
| if (!failed && type == null) { |
| try { |
| type = produce(); |
| } catch (Throwable e) { |
| failed = true; |
| } |
| } |
| return type; |
| } |
| |
| private static final ImageTypeProducer [] defaultTypes = |
| new ImageTypeProducer [JPEG.NUM_JCS_CODES]; |
| |
| public static synchronized ImageTypeProducer getTypeProducer(int csCode) { |
| if (csCode < 0 || csCode >= JPEG.NUM_JCS_CODES) { |
| return null; |
| } |
| if (defaultTypes[csCode] == null) { |
| defaultTypes[csCode] = new ImageTypeProducer(csCode); |
| } |
| return defaultTypes[csCode]; |
| } |
| |
| protected ImageTypeSpecifier produce() { |
| switch (csCode) { |
| case JPEG.JCS_GRAYSCALE: |
| return ImageTypeSpecifier.createFromBufferedImageType |
| (BufferedImage.TYPE_BYTE_GRAY); |
| case JPEG.JCS_YCbCr: |
| //there is no YCbCr raw type so by default we assume it as RGB |
| case JPEG.JCS_RGB: |
| return ImageTypeSpecifier.createInterleaved(JPEG.JCS.sRGB, |
| JPEG.bOffsRGB, |
| DataBuffer.TYPE_BYTE, |
| false, |
| false); |
| case JPEG.JCS_RGBA: |
| return ImageTypeSpecifier.createPacked(JPEG.JCS.sRGB, |
| 0xff000000, |
| 0x00ff0000, |
| 0x0000ff00, |
| 0x000000ff, |
| DataBuffer.TYPE_INT, |
| false); |
| case JPEG.JCS_YCC: |
| if (JPEG.JCS.getYCC() != null) { |
| return ImageTypeSpecifier.createInterleaved( |
| JPEG.JCS.getYCC(), |
| JPEG.bandOffsets[2], |
| DataBuffer.TYPE_BYTE, |
| false, |
| false); |
| } else { |
| return null; |
| } |
| case JPEG.JCS_YCCA: |
| if (JPEG.JCS.getYCC() != null) { |
| return ImageTypeSpecifier.createInterleaved( |
| JPEG.JCS.getYCC(), |
| JPEG.bandOffsets[3], |
| DataBuffer.TYPE_BYTE, |
| true, |
| false); |
| } else { |
| return null; |
| } |
| default: |
| return null; |
| } |
| } |
| } |