/*
 * Copyright (c) 2000, 2018, 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.
/** * 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
/** 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.
/** * List of stream positions for images, reinitialized every time * a new input source is set. */
private List imagePositions = null;
The number of images in the stream, or 0.
/** * 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("jpeg"); 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.
/** * 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.
/** * 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.
/** * 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.
/** * 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
/** Set by setImageData native code callback */
private int width;
Set by setImageData native code callback
/** Set by setImageData native code callback */
private int height;
Set by setImageData native code callback. A modified IJG+NIFTY colorspace code.
/** * 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.
/** * Set by setImageData native code callback. A modified * IJG+NIFTY colorspace code. */
private int outColorSpaceCode;
Set by setImageData native code callback
/** Set by setImageData native code callback */
private int numComponents;
Set by setImageData native code callback
/** Set by setImageData native code callback */
private ColorSpace iccCS = null;
If we need to post-convert in Java, convert with this op
/** If we need to post-convert in Java, convert with this op */
private ColorConvertOp convert = null;
The image we are going to fill
/** The image we are going to fill */
private BufferedImage image = null;
An intermediate Raster to hold decoded data
/** An intermediate Raster to hold decoded data */
private WritableRaster raster = null;
A view of our target Raster that we can setRect to
/** A view of our target Raster that we can setRect to */
private WritableRaster target = null;
The databuffer for the above Raster
/** The databuffer for the above Raster */
private DataBufferByte buffer = null;
The region in the destination where we will write pixels
/** The region in the destination where we will write pixels */
private Rectangle destROI = null;
The list of destination bands, if any
/** The list of destination bands, if any */
private int [] destinationBands = null;
Stream metadata, cached, even when the stream is changed.
/** Stream metadata, cached, even when the stream is changed. */
private JPEGMetadata streamMetadata = null;
Image metadata, valid for the imageMetadataIndex only.
/** 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.
/** * 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.
/** * 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.
/** * 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
/** * Set to true once stream has been checked for stream metadata */
private boolean tablesOnlyChecked = false;
The referent to be registered with the Disposer.
/** The referent to be registered with the Disposer. */
private Object disposerReferent = new Object();
The DisposerRecord that handles the actual disposal of this reader.
/** The DisposerRecord that handles the actual disposal of this reader. */
private DisposerRecord disposerRecord;
Sets up static C structures.
/** 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.
/** 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.
/** * 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.
/** * 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.
Returns:number of bytes read from the stream.
/** * 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.
Params:
  • n –
Throws:
Returns:
/** * 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(new Long(iis.getStreamPosition())); } } else { // Not tables only, so add original pos to the list imagePositions.add(new Long(savePos)); // And set current image since we've read it now currentImage = 0; } // If the image positions list is empty as in the case of a tables-only // stream, then attempting to access the element at index // imagePositions.size() - 1 will cause an IndexOutOfBoundsException. if (seekForwardOnly && !imagePositions.isEmpty()) { Long pos = (Long) 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(); } } private void skipPastImage(int imageIndex) { cbLock.lock(); try { gotoImage(imageIndex); skipImage(); } catch (IOException | IndexOutOfBoundsException e) { } finally { cbLock.unlock(); } } 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.
Throws:
  • IllegalStateException – if the input source has not been set.
  • IndexOutOfBoundsException – if the supplied index is out of bounds.
/** * 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 the image positions list is empty as in the case of a tables-only // stream, then no image data can be read. if (imagePositions.isEmpty()) { throw new IIOException("No image data present to read"); } if (imageIndex < imagePositions.size()) { iis.seek(((Long)(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 = (Long) 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 = new Long(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. If the stream ends before an EOI marker is encountered, an IndexOutOfBoundsException is thrown.
/** * 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. * 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"); } boolean foundFF = false; for (int byteval = iis.read(); byteval != -1; byteval = iis.read()) { if (foundFF == true) { if (byteval == JPEG.EOI) { return; } } foundFF = (byteval == 0xff) ? true : false; } throw new IndexOutOfBoundsException(); }
Returns true if there is an image beyond the current stream position. Does not disturb the stream position.
/** * Returns <code>true</code> 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.
/** * 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.
/** * 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 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 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.
/** * Read in the header information starting from the current * stream position, returning <code>true</code> 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</code>, 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.
/** * 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 getImageTypes(int imageIndex) throws IOException { setThreadLock(); try { return getImageTypesOnThread(imageIndex); } finally { clearThreadLock(); } } private Iterator 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 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.
/** * 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</code>. * 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.
/** * 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 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 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(imageIndex, 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.
/** * 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 true if the read was aborted.
/** * Returns <code>true</code> if the read was aborted. */
private native boolean readImage(int imageIndex, 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.
/** 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.
/** 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.
/** * 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.
/** * 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 synchronized static 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_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; } } }