| /* |
| * Copyright (c) 2000, 2007, 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.ImageWriter; |
| import javax.imageio.ImageWriteParam; |
| import javax.imageio.IIOImage; |
| import javax.imageio.ImageTypeSpecifier; |
| import javax.imageio.metadata.IIOMetadata; |
| import javax.imageio.metadata.IIOMetadataFormatImpl; |
| import javax.imageio.metadata.IIOInvalidTreeException; |
| import javax.imageio.spi.ImageWriterSpi; |
| import javax.imageio.stream.ImageOutputStream; |
| import javax.imageio.plugins.jpeg.JPEGImageWriteParam; |
| import javax.imageio.plugins.jpeg.JPEGQTable; |
| import javax.imageio.plugins.jpeg.JPEGHuffmanTable; |
| |
| import org.w3c.dom.Node; |
| |
| import java.awt.image.Raster; |
| import java.awt.image.WritableRaster; |
| import java.awt.image.SampleModel; |
| 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.awt.image.RenderedImage; |
| import java.awt.image.BufferedImage; |
| import java.awt.color.ColorSpace; |
| import java.awt.color.ICC_ColorSpace; |
| import java.awt.color.ICC_Profile; |
| import java.awt.Dimension; |
| import java.awt.Rectangle; |
| import java.awt.Transparency; |
| |
| import java.io.IOException; |
| |
| import java.util.List; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| |
| import sun.java2d.Disposer; |
| import sun.java2d.DisposerRecord; |
| |
| public class JPEGImageWriter extends ImageWriter { |
| |
| ///////// Private variables |
| |
| 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 output stream we write to */ |
| private ImageOutputStream ios = null; |
| |
| /** The Raster we will write from */ |
| private Raster srcRas = null; |
| |
| /** An intermediate Raster holding compressor-friendly data */ |
| private WritableRaster raster = null; |
| |
| /** |
| * Set to true if we are writing an image with an |
| * indexed ColorModel |
| */ |
| private boolean indexed = false; |
| private IndexColorModel indexCM = null; |
| |
| private boolean convertTosRGB = false; // Used by PhotoYCC only |
| private WritableRaster converted = null; |
| |
| private boolean isAlphaPremultiplied = false; |
| private ColorModel srcCM = null; |
| |
| /** |
| * If there are thumbnails to be written, this is the list. |
| */ |
| private List thumbnails = null; |
| |
| /** |
| * If metadata should include an icc profile, store it here. |
| */ |
| private ICC_Profile iccProfile = null; |
| |
| private int sourceXOffset = 0; |
| private int sourceYOffset = 0; |
| private int sourceWidth = 0; |
| private int [] srcBands = null; |
| private int sourceHeight = 0; |
| |
| /** Used when calling listeners */ |
| private int currentImage = 0; |
| |
| private ColorConvertOp convertOp = null; |
| |
| private JPEGQTable [] streamQTables = null; |
| private JPEGHuffmanTable[] streamDCHuffmanTables = null; |
| private JPEGHuffmanTable[] streamACHuffmanTables = null; |
| |
| // Parameters for writing metadata |
| private boolean ignoreJFIF = false; // If it's there, use it |
| private boolean forceJFIF = false; // Add one for the thumbnails |
| private boolean ignoreAdobe = false; // If it's there, use it |
| private int newAdobeTransform = JPEG.ADOBE_IMPOSSIBLE; // Change if needed |
| private boolean writeDefaultJFIF = false; |
| private boolean writeAdobe = false; |
| private JPEGMetadata metadata = null; |
| |
| private boolean sequencePrepared = false; |
| |
| private int numScans = 0; |
| |
| /** The referent to be registered with the Disposer. */ |
| private Object disposerReferent = new Object(); |
| |
| /** The DisposerRecord that handles the actual disposal of this writer. */ |
| private DisposerRecord disposerRecord; |
| |
| ///////// End of Private variables |
| |
| ///////// Protected variables |
| |
| protected static final int WARNING_DEST_IGNORED = 0; |
| protected static final int WARNING_STREAM_METADATA_IGNORED = 1; |
| protected static final int WARNING_DEST_METADATA_COMP_MISMATCH = 2; |
| protected static final int WARNING_DEST_METADATA_JFIF_MISMATCH = 3; |
| protected static final int WARNING_DEST_METADATA_ADOBE_MISMATCH = 4; |
| protected static final int WARNING_IMAGE_METADATA_JFIF_MISMATCH = 5; |
| protected static final int WARNING_IMAGE_METADATA_ADOBE_MISMATCH = 6; |
| protected static final int WARNING_METADATA_NOT_JPEG_FOR_RASTER = 7; |
| protected static final int WARNING_NO_BANDS_ON_INDEXED = 8; |
| protected static final int WARNING_ILLEGAL_THUMBNAIL = 9; |
| protected static final int WARNING_IGNORING_THUMBS = 10; |
| protected static final int WARNING_FORCING_JFIF = 11; |
| protected static final int WARNING_THUMB_CLIPPED = 12; |
| protected static final int WARNING_METADATA_ADJUSTED_FOR_THUMB = 13; |
| protected static final int WARNING_NO_RGB_THUMB_AS_INDEXED = 14; |
| protected static final int WARNING_NO_GRAY_THUMB_AS_INDEXED = 15; |
| |
| private static final int MAX_WARNING = WARNING_NO_GRAY_THUMB_AS_INDEXED; |
| |
| ///////// End of Protected variables |
| |
| ///////// static initializer |
| |
| static { |
| java.security.AccessController.doPrivileged( |
| new sun.security.action.LoadLibraryAction("jpeg")); |
| initWriterIDs(ImageOutputStream.class, |
| JPEGQTable.class, |
| JPEGHuffmanTable.class); |
| } |
| |
| //////// Public API |
| |
| public JPEGImageWriter(ImageWriterSpi originator) { |
| super(originator); |
| structPointer = initJPEGImageWriter(); |
| disposerRecord = new JPEGWriterDisposerRecord(structPointer); |
| Disposer.addRecord(disposerReferent, disposerRecord); |
| } |
| |
| public void setOutput(Object output) { |
| setThreadLock(); |
| try { |
| super.setOutput(output); // validates output |
| resetInternalState(); |
| ios = (ImageOutputStream) output; // so this will always work |
| // Set the native destination |
| setDest(structPointer, ios); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public ImageWriteParam getDefaultWriteParam() { |
| return new JPEGImageWriteParam(null); |
| } |
| |
| public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { |
| setThreadLock(); |
| try { |
| return new JPEGMetadata(param, this); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public IIOMetadata |
| getDefaultImageMetadata(ImageTypeSpecifier imageType, |
| ImageWriteParam param) { |
| setThreadLock(); |
| try { |
| return new JPEGMetadata(imageType, param, this); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public IIOMetadata convertStreamMetadata(IIOMetadata inData, |
| ImageWriteParam param) { |
| // There isn't much we can do. If it's one of ours, then |
| // return it. Otherwise just return null. We use it only |
| // for tables, so we can't get a default and modify it, |
| // as this will usually not be what is intended. |
| if (inData instanceof JPEGMetadata) { |
| JPEGMetadata jpegData = (JPEGMetadata) inData; |
| if (jpegData.isStream) { |
| return inData; |
| } |
| } |
| return null; |
| } |
| |
| public IIOMetadata |
| convertImageMetadata(IIOMetadata inData, |
| ImageTypeSpecifier imageType, |
| ImageWriteParam param) { |
| setThreadLock(); |
| try { |
| return convertImageMetadataOnThread(inData, imageType, param); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private IIOMetadata |
| convertImageMetadataOnThread(IIOMetadata inData, |
| ImageTypeSpecifier imageType, |
| ImageWriteParam param) { |
| // If it's one of ours, just return it |
| if (inData instanceof JPEGMetadata) { |
| JPEGMetadata jpegData = (JPEGMetadata) inData; |
| if (!jpegData.isStream) { |
| return inData; |
| } else { |
| // Can't convert stream metadata to image metadata |
| // XXX Maybe this should put out a warning? |
| return null; |
| } |
| } |
| // If it's not one of ours, create a default and set it from |
| // the standard tree from the input, if it exists. |
| if (inData.isStandardMetadataFormatSupported()) { |
| String formatName = |
| IIOMetadataFormatImpl.standardMetadataFormatName; |
| Node tree = inData.getAsTree(formatName); |
| if (tree != null) { |
| JPEGMetadata jpegData = new JPEGMetadata(imageType, |
| param, |
| this); |
| try { |
| jpegData.setFromTree(formatName, tree); |
| } catch (IIOInvalidTreeException e) { |
| // Other plug-in generates bogus standard tree |
| // XXX Maybe this should put out a warning? |
| return null; |
| } |
| |
| return jpegData; |
| } |
| } |
| return null; |
| } |
| |
| public int getNumThumbnailsSupported(ImageTypeSpecifier imageType, |
| ImageWriteParam param, |
| IIOMetadata streamMetadata, |
| IIOMetadata imageMetadata) { |
| if (jfifOK(imageType, param, streamMetadata, imageMetadata)) { |
| return Integer.MAX_VALUE; |
| } |
| return 0; |
| } |
| |
| static final Dimension [] preferredThumbSizes = {new Dimension(1, 1), |
| new Dimension(255, 255)}; |
| |
| public Dimension[] getPreferredThumbnailSizes(ImageTypeSpecifier imageType, |
| ImageWriteParam param, |
| IIOMetadata streamMetadata, |
| IIOMetadata imageMetadata) { |
| if (jfifOK(imageType, param, streamMetadata, imageMetadata)) { |
| return (Dimension [])preferredThumbSizes.clone(); |
| } |
| return null; |
| } |
| |
| private boolean jfifOK(ImageTypeSpecifier imageType, |
| ImageWriteParam param, |
| IIOMetadata streamMetadata, |
| IIOMetadata imageMetadata) { |
| // If the image type and metadata are JFIF compatible, return true |
| if ((imageType != null) && |
| (!JPEG.isJFIFcompliant(imageType, true))) { |
| return false; |
| } |
| if (imageMetadata != null) { |
| JPEGMetadata metadata = null; |
| if (imageMetadata instanceof JPEGMetadata) { |
| metadata = (JPEGMetadata) imageMetadata; |
| } else { |
| metadata = (JPEGMetadata)convertImageMetadata(imageMetadata, |
| imageType, |
| param); |
| } |
| // metadata must have a jfif node |
| if (metadata.findMarkerSegment |
| (JFIFMarkerSegment.class, true) == null){ |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public boolean canWriteRasters() { |
| return true; |
| } |
| |
| public void write(IIOMetadata streamMetadata, |
| IIOImage image, |
| ImageWriteParam param) throws IOException { |
| setThreadLock(); |
| try { |
| writeOnThread(streamMetadata, image, param); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private void writeOnThread(IIOMetadata streamMetadata, |
| IIOImage image, |
| ImageWriteParam param) throws IOException { |
| |
| if (ios == null) { |
| throw new IllegalStateException("Output has not been set!"); |
| } |
| |
| if (image == null) { |
| throw new IllegalArgumentException("image is null!"); |
| } |
| |
| // if streamMetadata is not null, issue a warning |
| if (streamMetadata != null) { |
| warningOccurred(WARNING_STREAM_METADATA_IGNORED); |
| } |
| |
| // Obtain the raster and image, if there is one |
| boolean rasterOnly = image.hasRaster(); |
| |
| RenderedImage rimage = null; |
| if (rasterOnly) { |
| srcRas = image.getRaster(); |
| } else { |
| rimage = image.getRenderedImage(); |
| if (rimage instanceof BufferedImage) { |
| // Use the Raster directly. |
| srcRas = ((BufferedImage)rimage).getRaster(); |
| } else if (rimage.getNumXTiles() == 1 && |
| rimage.getNumYTiles() == 1) |
| { |
| // Get the unique tile. |
| srcRas = rimage.getTile(rimage.getMinTileX(), |
| rimage.getMinTileY()); |
| |
| // Ensure the Raster has dimensions of the image, |
| // as the tile dimensions might differ. |
| if (srcRas.getWidth() != rimage.getWidth() || |
| srcRas.getHeight() != rimage.getHeight()) |
| { |
| srcRas = srcRas.createChild(srcRas.getMinX(), |
| srcRas.getMinY(), |
| rimage.getWidth(), |
| rimage.getHeight(), |
| srcRas.getMinX(), |
| srcRas.getMinY(), |
| null); |
| } |
| } else { |
| // Image is tiled so get a contiguous raster by copying. |
| srcRas = rimage.getData(); |
| } |
| } |
| |
| // Now determine if we are using a band subset |
| |
| // By default, we are using all source bands |
| int numSrcBands = srcRas.getNumBands(); |
| indexed = false; |
| indexCM = null; |
| ColorModel cm = null; |
| ColorSpace cs = null; |
| isAlphaPremultiplied = false; |
| srcCM = null; |
| if (!rasterOnly) { |
| cm = rimage.getColorModel(); |
| if (cm != null) { |
| cs = cm.getColorSpace(); |
| if (cm instanceof IndexColorModel) { |
| indexed = true; |
| indexCM = (IndexColorModel) cm; |
| numSrcBands = cm.getNumComponents(); |
| } |
| if (cm.isAlphaPremultiplied()) { |
| isAlphaPremultiplied = true; |
| srcCM = cm; |
| } |
| } |
| } |
| |
| srcBands = JPEG.bandOffsets[numSrcBands-1]; |
| int numBandsUsed = numSrcBands; |
| // Consult the param to determine if we're writing a subset |
| |
| if (param != null) { |
| int[] sBands = param.getSourceBands(); |
| if (sBands != null) { |
| if (indexed) { |
| warningOccurred(WARNING_NO_BANDS_ON_INDEXED); |
| } else { |
| srcBands = sBands; |
| numBandsUsed = srcBands.length; |
| if (numBandsUsed > numSrcBands) { |
| throw new IIOException |
| ("ImageWriteParam specifies too many source bands"); |
| } |
| } |
| } |
| } |
| |
| boolean usingBandSubset = (numBandsUsed != numSrcBands); |
| boolean fullImage = ((!rasterOnly) && (!usingBandSubset)); |
| |
| int [] bandSizes = null; |
| if (!indexed) { |
| bandSizes = srcRas.getSampleModel().getSampleSize(); |
| // If this is a subset, we must adjust bandSizes |
| if (usingBandSubset) { |
| int [] temp = new int [numBandsUsed]; |
| for (int i = 0; i < numBandsUsed; i++) { |
| temp[i] = bandSizes[srcBands[i]]; |
| } |
| bandSizes = temp; |
| } |
| } else { |
| int [] tempSize = srcRas.getSampleModel().getSampleSize(); |
| bandSizes = new int [numSrcBands]; |
| for (int i = 0; i < numSrcBands; i++) { |
| bandSizes[i] = tempSize[0]; // All the same |
| } |
| } |
| |
| for (int i = 0; i < bandSizes.length; i++) { |
| // 4450894 part 1: The IJG libraries are compiled so they only |
| // handle <= 8-bit samples. We now check the band sizes and throw |
| // an exception for images, such as USHORT_GRAY, with > 8 bits |
| // per sample. |
| if (bandSizes[i] > 8) { |
| throw new IIOException("Sample size must be <= 8"); |
| } |
| // 4450894 part 2: We expand IndexColorModel images to full 24- |
| // or 32-bit in grabPixels() for each scanline. For indexed |
| // images such as BYTE_BINARY, we need to ensure that we update |
| // bandSizes to account for the scaling from 1-bit band sizes |
| // to 8-bit. |
| if (indexed) { |
| bandSizes[i] = 8; |
| } |
| } |
| |
| if (debug) { |
| System.out.println("numSrcBands is " + numSrcBands); |
| System.out.println("numBandsUsed is " + numBandsUsed); |
| System.out.println("usingBandSubset is " + usingBandSubset); |
| System.out.println("fullImage is " + fullImage); |
| System.out.print("Band sizes:"); |
| for (int i = 0; i< bandSizes.length; i++) { |
| System.out.print(" " + bandSizes[i]); |
| } |
| System.out.println(); |
| } |
| |
| // Destination type, if there is one |
| ImageTypeSpecifier destType = null; |
| if (param != null) { |
| destType = param.getDestinationType(); |
| // Ignore dest type if we are writing a complete image |
| if ((fullImage) && (destType != null)) { |
| warningOccurred(WARNING_DEST_IGNORED); |
| destType = null; |
| } |
| } |
| |
| // Examine the param |
| |
| sourceXOffset = srcRas.getMinX(); |
| sourceYOffset = srcRas.getMinY(); |
| int imageWidth = srcRas.getWidth(); |
| int imageHeight = srcRas.getHeight(); |
| sourceWidth = imageWidth; |
| sourceHeight = imageHeight; |
| int periodX = 1; |
| int periodY = 1; |
| int gridX = 0; |
| int gridY = 0; |
| JPEGQTable [] qTables = null; |
| JPEGHuffmanTable[] DCHuffmanTables = null; |
| JPEGHuffmanTable[] ACHuffmanTables = null; |
| boolean optimizeHuffman = false; |
| JPEGImageWriteParam jparam = null; |
| int progressiveMode = ImageWriteParam.MODE_DISABLED; |
| |
| if (param != null) { |
| |
| Rectangle sourceRegion = param.getSourceRegion(); |
| if (sourceRegion != null) { |
| Rectangle imageBounds = new Rectangle(sourceXOffset, |
| sourceYOffset, |
| sourceWidth, |
| sourceHeight); |
| sourceRegion = sourceRegion.intersection(imageBounds); |
| sourceXOffset = sourceRegion.x; |
| sourceYOffset = sourceRegion.y; |
| sourceWidth = sourceRegion.width; |
| sourceHeight = sourceRegion.height; |
| } |
| |
| if (sourceWidth + sourceXOffset > imageWidth) { |
| sourceWidth = imageWidth - sourceXOffset; |
| } |
| if (sourceHeight + sourceYOffset > imageHeight) { |
| sourceHeight = imageHeight - sourceYOffset; |
| } |
| |
| periodX = param.getSourceXSubsampling(); |
| periodY = param.getSourceYSubsampling(); |
| gridX = param.getSubsamplingXOffset(); |
| gridY = param.getSubsamplingYOffset(); |
| |
| switch(param.getCompressionMode()) { |
| case ImageWriteParam.MODE_DISABLED: |
| throw new IIOException("JPEG compression cannot be disabled"); |
| case ImageWriteParam.MODE_EXPLICIT: |
| float quality = param.getCompressionQuality(); |
| quality = JPEG.convertToLinearQuality(quality); |
| qTables = new JPEGQTable[2]; |
| qTables[0] = JPEGQTable.K1Luminance.getScaledInstance |
| (quality, true); |
| qTables[1] = JPEGQTable.K2Chrominance.getScaledInstance |
| (quality, true); |
| break; |
| case ImageWriteParam.MODE_DEFAULT: |
| qTables = new JPEGQTable[2]; |
| qTables[0] = JPEGQTable.K1Div2Luminance; |
| qTables[1] = JPEGQTable.K2Div2Chrominance; |
| break; |
| // We'll handle the metadata case later |
| } |
| |
| progressiveMode = param.getProgressiveMode(); |
| |
| if (param instanceof JPEGImageWriteParam) { |
| jparam = (JPEGImageWriteParam)param; |
| optimizeHuffman = jparam.getOptimizeHuffmanTables(); |
| } |
| } |
| |
| // Now examine the metadata |
| IIOMetadata mdata = image.getMetadata(); |
| if (mdata != null) { |
| if (mdata instanceof JPEGMetadata) { |
| metadata = (JPEGMetadata) mdata; |
| if (debug) { |
| System.out.println |
| ("We have metadata, and it's JPEG metadata"); |
| } |
| } else { |
| if (!rasterOnly) { |
| ImageTypeSpecifier type = destType; |
| if (type == null) { |
| type = new ImageTypeSpecifier(rimage); |
| } |
| metadata = (JPEGMetadata) convertImageMetadata(mdata, |
| type, |
| param); |
| } else { |
| warningOccurred(WARNING_METADATA_NOT_JPEG_FOR_RASTER); |
| } |
| } |
| } |
| |
| // First set a default state |
| |
| ignoreJFIF = false; // If it's there, use it |
| ignoreAdobe = false; // If it's there, use it |
| newAdobeTransform = JPEG.ADOBE_IMPOSSIBLE; // Change if needed |
| writeDefaultJFIF = false; |
| writeAdobe = false; |
| |
| // By default we'll do no conversion: |
| int inCsType = JPEG.JCS_UNKNOWN; |
| int outCsType = JPEG.JCS_UNKNOWN; |
| |
| JFIFMarkerSegment jfif = null; |
| AdobeMarkerSegment adobe = null; |
| SOFMarkerSegment sof = null; |
| |
| if (metadata != null) { |
| jfif = (JFIFMarkerSegment) metadata.findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| adobe = (AdobeMarkerSegment) metadata.findMarkerSegment |
| (AdobeMarkerSegment.class, true); |
| sof = (SOFMarkerSegment) metadata.findMarkerSegment |
| (SOFMarkerSegment.class, true); |
| } |
| |
| iccProfile = null; // By default don't write one |
| convertTosRGB = false; // PhotoYCC does this |
| converted = null; |
| |
| if (destType != null) { |
| if (numBandsUsed != destType.getNumBands()) { |
| throw new IIOException |
| ("Number of source bands != number of destination bands"); |
| } |
| cs = destType.getColorModel().getColorSpace(); |
| // Check the metadata against the destination type |
| if (metadata != null) { |
| checkSOFBands(sof, numBandsUsed); |
| |
| checkJFIF(jfif, destType, false); |
| // Do we want to write an ICC profile? |
| if ((jfif != null) && (ignoreJFIF == false)) { |
| if (JPEG.isNonStandardICC(cs)) { |
| iccProfile = ((ICC_ColorSpace) cs).getProfile(); |
| } |
| } |
| checkAdobe(adobe, destType, false); |
| |
| } else { // no metadata, but there is a dest type |
| // If we can add a JFIF or an Adobe marker segment, do so |
| if (JPEG.isJFIFcompliant(destType, false)) { |
| writeDefaultJFIF = true; |
| // Do we want to write an ICC profile? |
| if (JPEG.isNonStandardICC(cs)) { |
| iccProfile = ((ICC_ColorSpace) cs).getProfile(); |
| } |
| } else { |
| int transform = JPEG.transformForType(destType, false); |
| if (transform != JPEG.ADOBE_IMPOSSIBLE) { |
| writeAdobe = true; |
| newAdobeTransform = transform; |
| } |
| } |
| // re-create the metadata |
| metadata = new JPEGMetadata(destType, null, this); |
| } |
| inCsType = getSrcCSType(destType); |
| outCsType = getDefaultDestCSType(destType); |
| } else { // no destination type |
| if (metadata == null) { |
| if (fullImage) { // no dest, no metadata, full image |
| // Use default metadata matching the image and param |
| metadata = new JPEGMetadata(new ImageTypeSpecifier(rimage), |
| param, this); |
| if (metadata.findMarkerSegment |
| (JFIFMarkerSegment.class, true) != null) { |
| cs = rimage.getColorModel().getColorSpace(); |
| if (JPEG.isNonStandardICC(cs)) { |
| iccProfile = ((ICC_ColorSpace) cs).getProfile(); |
| } |
| } |
| |
| inCsType = getSrcCSType(rimage); |
| outCsType = getDefaultDestCSType(rimage); |
| } |
| // else no dest, no metadata, not an image, |
| // so no special headers, no color conversion |
| } else { // no dest type, but there is metadata |
| checkSOFBands(sof, numBandsUsed); |
| if (fullImage) { // no dest, metadata, image |
| // Check that the metadata and the image match |
| |
| ImageTypeSpecifier inputType = |
| new ImageTypeSpecifier(rimage); |
| |
| inCsType = getSrcCSType(rimage); |
| |
| if (cm != null) { |
| boolean alpha = cm.hasAlpha(); |
| switch (cs.getType()) { |
| case ColorSpace.TYPE_GRAY: |
| if (!alpha) { |
| outCsType = JPEG.JCS_GRAYSCALE; |
| } else { |
| if (jfif != null) { |
| ignoreJFIF = true; |
| warningOccurred |
| (WARNING_IMAGE_METADATA_JFIF_MISMATCH); |
| } |
| // out colorspace remains unknown |
| } |
| if ((adobe != null) |
| && (adobe.transform != JPEG.ADOBE_UNKNOWN)) { |
| newAdobeTransform = JPEG.ADOBE_UNKNOWN; |
| warningOccurred |
| (WARNING_IMAGE_METADATA_ADOBE_MISMATCH); |
| } |
| break; |
| case ColorSpace.TYPE_RGB: |
| if (!alpha) { |
| if (jfif != null) { |
| outCsType = JPEG.JCS_YCbCr; |
| if (JPEG.isNonStandardICC(cs) |
| || ((cs instanceof ICC_ColorSpace) |
| && (jfif.iccSegment != null))) { |
| iccProfile = |
| ((ICC_ColorSpace) cs).getProfile(); |
| } |
| } else if (adobe != null) { |
| switch (adobe.transform) { |
| case JPEG.ADOBE_UNKNOWN: |
| outCsType = JPEG.JCS_RGB; |
| break; |
| case JPEG.ADOBE_YCC: |
| outCsType = JPEG.JCS_YCbCr; |
| break; |
| default: |
| warningOccurred |
| (WARNING_IMAGE_METADATA_ADOBE_MISMATCH); |
| newAdobeTransform = JPEG.ADOBE_UNKNOWN; |
| outCsType = JPEG.JCS_RGB; |
| break; |
| } |
| } else { |
| // consult the ids |
| int outCS = sof.getIDencodedCSType(); |
| // if they don't resolve it, |
| // consult the sampling factors |
| if (outCS != JPEG.JCS_UNKNOWN) { |
| outCsType = outCS; |
| } else { |
| boolean subsampled = |
| isSubsampled(sof.componentSpecs); |
| if (subsampled) { |
| outCsType = JPEG.JCS_YCbCr; |
| } else { |
| outCsType = JPEG.JCS_RGB; |
| } |
| } |
| } |
| } else { // RGBA |
| if (jfif != null) { |
| ignoreJFIF = true; |
| warningOccurred |
| (WARNING_IMAGE_METADATA_JFIF_MISMATCH); |
| } |
| if (adobe != null) { |
| if (adobe.transform |
| != JPEG.ADOBE_UNKNOWN) { |
| newAdobeTransform = JPEG.ADOBE_UNKNOWN; |
| warningOccurred |
| (WARNING_IMAGE_METADATA_ADOBE_MISMATCH); |
| } |
| outCsType = JPEG.JCS_RGBA; |
| } else { |
| // consult the ids |
| int outCS = sof.getIDencodedCSType(); |
| // if they don't resolve it, |
| // consult the sampling factors |
| if (outCS != JPEG.JCS_UNKNOWN) { |
| outCsType = outCS; |
| } else { |
| boolean subsampled = |
| isSubsampled(sof.componentSpecs); |
| outCsType = subsampled ? |
| JPEG.JCS_YCbCrA : JPEG.JCS_RGBA; |
| } |
| } |
| } |
| break; |
| case ColorSpace.TYPE_3CLR: |
| if (cs == JPEG.JCS.getYCC()) { |
| if (!alpha) { |
| if (jfif != null) { |
| convertTosRGB = true; |
| convertOp = |
| new ColorConvertOp(cs, |
| JPEG.JCS.sRGB, |
| null); |
| outCsType = JPEG.JCS_YCbCr; |
| } else if (adobe != null) { |
| if (adobe.transform |
| != JPEG.ADOBE_YCC) { |
| newAdobeTransform = JPEG.ADOBE_YCC; |
| warningOccurred |
| (WARNING_IMAGE_METADATA_ADOBE_MISMATCH); |
| } |
| outCsType = JPEG.JCS_YCC; |
| } else { |
| outCsType = JPEG.JCS_YCC; |
| } |
| } else { // PhotoYCCA |
| if (jfif != null) { |
| ignoreJFIF = true; |
| warningOccurred |
| (WARNING_IMAGE_METADATA_JFIF_MISMATCH); |
| } else if (adobe != null) { |
| if (adobe.transform |
| != JPEG.ADOBE_UNKNOWN) { |
| newAdobeTransform |
| = JPEG.ADOBE_UNKNOWN; |
| warningOccurred |
| (WARNING_IMAGE_METADATA_ADOBE_MISMATCH); |
| } |
| } |
| outCsType = JPEG.JCS_YCCA; |
| } |
| } |
| } |
| } |
| } // else no dest, metadata, not an image. Defaults ok |
| } |
| } |
| |
| boolean metadataProgressive = false; |
| int [] scans = null; |
| |
| if (metadata != null) { |
| if (sof == null) { |
| sof = (SOFMarkerSegment) metadata.findMarkerSegment |
| (SOFMarkerSegment.class, true); |
| } |
| if ((sof != null) && (sof.tag == JPEG.SOF2)) { |
| metadataProgressive = true; |
| if (progressiveMode == ImageWriteParam.MODE_COPY_FROM_METADATA) { |
| scans = collectScans(metadata, sof); // Might still be null |
| } else { |
| numScans = 0; |
| } |
| } |
| if (jfif == null) { |
| jfif = (JFIFMarkerSegment) metadata.findMarkerSegment |
| (JFIFMarkerSegment.class, true); |
| } |
| } |
| |
| thumbnails = image.getThumbnails(); |
| int numThumbs = image.getNumThumbnails(); |
| forceJFIF = false; |
| // determine if thumbnails can be written |
| // If we are going to add a default JFIF marker segment, |
| // then thumbnails can be written |
| if (!writeDefaultJFIF) { |
| // If there is no metadata, then we can't write thumbnails |
| if (metadata == null) { |
| thumbnails = null; |
| if (numThumbs != 0) { |
| warningOccurred(WARNING_IGNORING_THUMBS); |
| } |
| } else { |
| // There is metadata |
| // If we are writing a raster or subbands, |
| // then the user must specify JFIF on the metadata |
| if (fullImage == false) { |
| if (jfif == null) { |
| thumbnails = null; // Or we can't include thumbnails |
| if (numThumbs != 0) { |
| warningOccurred(WARNING_IGNORING_THUMBS); |
| } |
| } |
| } else { // It is a full image, and there is metadata |
| if (jfif == null) { // Not JFIF |
| // Can it have JFIF? |
| if ((outCsType == JPEG.JCS_GRAYSCALE) |
| || (outCsType == JPEG.JCS_YCbCr)) { |
| if (numThumbs != 0) { |
| forceJFIF = true; |
| warningOccurred(WARNING_FORCING_JFIF); |
| } |
| } else { // Nope, not JFIF-compatible |
| thumbnails = null; |
| if (numThumbs != 0) { |
| warningOccurred(WARNING_IGNORING_THUMBS); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Set up a boolean to indicate whether we need to call back to |
| // write metadata |
| boolean haveMetadata = |
| ((metadata != null) || writeDefaultJFIF || writeAdobe); |
| |
| // Now that we have dealt with metadata, finalize our tables set up |
| |
| // Are we going to write tables? By default, yes. |
| boolean writeDQT = true; |
| boolean writeDHT = true; |
| |
| // But if the metadata has no tables, no. |
| DQTMarkerSegment dqt = null; |
| DHTMarkerSegment dht = null; |
| |
| int restartInterval = 0; |
| |
| if (metadata != null) { |
| dqt = (DQTMarkerSegment) metadata.findMarkerSegment |
| (DQTMarkerSegment.class, true); |
| dht = (DHTMarkerSegment) metadata.findMarkerSegment |
| (DHTMarkerSegment.class, true); |
| DRIMarkerSegment dri = |
| (DRIMarkerSegment) metadata.findMarkerSegment |
| (DRIMarkerSegment.class, true); |
| if (dri != null) { |
| restartInterval = dri.restartInterval; |
| } |
| |
| if (dqt == null) { |
| writeDQT = false; |
| } |
| if (dht == null) { |
| writeDHT = false; // Ignored if optimizeHuffman is true |
| } |
| } |
| |
| // Whether we write tables or not, we need to figure out which ones |
| // to use |
| if (qTables == null) { // Get them from metadata, or use defaults |
| if (dqt != null) { |
| qTables = collectQTablesFromMetadata(metadata); |
| } else if (streamQTables != null) { |
| qTables = streamQTables; |
| } else if ((jparam != null) && (jparam.areTablesSet())) { |
| qTables = jparam.getQTables(); |
| } else { |
| qTables = JPEG.getDefaultQTables(); |
| } |
| |
| } |
| |
| // If we are optimizing, we don't want any tables. |
| if (optimizeHuffman == false) { |
| // If they were for progressive scans, we can't use them. |
| if ((dht != null) && (metadataProgressive == false)) { |
| DCHuffmanTables = collectHTablesFromMetadata(metadata, true); |
| ACHuffmanTables = collectHTablesFromMetadata(metadata, false); |
| } else if (streamDCHuffmanTables != null) { |
| DCHuffmanTables = streamDCHuffmanTables; |
| ACHuffmanTables = streamACHuffmanTables; |
| } else if ((jparam != null) && (jparam.areTablesSet())) { |
| DCHuffmanTables = jparam.getDCHuffmanTables(); |
| ACHuffmanTables = jparam.getACHuffmanTables(); |
| } else { |
| DCHuffmanTables = JPEG.getDefaultHuffmanTables(true); |
| ACHuffmanTables = JPEG.getDefaultHuffmanTables(false); |
| } |
| } |
| |
| // By default, ids are 1 - N, no subsampling |
| int [] componentIds = new int[numBandsUsed]; |
| int [] HsamplingFactors = new int[numBandsUsed]; |
| int [] VsamplingFactors = new int[numBandsUsed]; |
| int [] QtableSelectors = new int[numBandsUsed]; |
| for (int i = 0; i < numBandsUsed; i++) { |
| componentIds[i] = i+1; // JFIF compatible |
| HsamplingFactors[i] = 1; |
| VsamplingFactors[i] = 1; |
| QtableSelectors[i] = 0; |
| } |
| |
| // Now override them with the contents of sof, if there is one, |
| if (sof != null) { |
| for (int i = 0; i < numBandsUsed; i++) { |
| if (forceJFIF == false) { // else use JFIF-compatible default |
| componentIds[i] = sof.componentSpecs[i].componentId; |
| } |
| HsamplingFactors[i] = sof.componentSpecs[i].HsamplingFactor; |
| VsamplingFactors[i] = sof.componentSpecs[i].VsamplingFactor; |
| QtableSelectors[i] = sof.componentSpecs[i].QtableSelector; |
| } |
| } |
| |
| sourceXOffset += gridX; |
| sourceWidth -= gridX; |
| sourceYOffset += gridY; |
| sourceHeight -= gridY; |
| |
| int destWidth = (sourceWidth + periodX - 1)/periodX; |
| int destHeight = (sourceHeight + periodY - 1)/periodY; |
| |
| // Create an appropriate 1-line databuffer for writing |
| int lineSize = sourceWidth*numBandsUsed; |
| |
| DataBufferByte buffer = new DataBufferByte(lineSize); |
| |
| // Create a raster from that |
| int [] bandOffs = JPEG.bandOffsets[numBandsUsed-1]; |
| |
| raster = Raster.createInterleavedRaster(buffer, |
| sourceWidth, 1, |
| lineSize, |
| numBandsUsed, |
| bandOffs, |
| null); |
| |
| // Call the writer, who will call back for every scanline |
| |
| processImageStarted(currentImage); |
| |
| boolean aborted = false; |
| |
| if (debug) { |
| System.out.println("inCsType: " + inCsType); |
| System.out.println("outCsType: " + outCsType); |
| } |
| |
| // Note that getData disables acceleration on buffer, but it is |
| // just a 1-line intermediate data transfer buffer that does not |
| // affect the acceleration of the source image. |
| aborted = writeImage(structPointer, |
| buffer.getData(), |
| inCsType, outCsType, |
| numBandsUsed, |
| bandSizes, |
| sourceWidth, |
| destWidth, destHeight, |
| periodX, periodY, |
| qTables, |
| writeDQT, |
| DCHuffmanTables, |
| ACHuffmanTables, |
| writeDHT, |
| optimizeHuffman, |
| (progressiveMode |
| != ImageWriteParam.MODE_DISABLED), |
| numScans, |
| scans, |
| componentIds, |
| HsamplingFactors, |
| VsamplingFactors, |
| QtableSelectors, |
| haveMetadata, |
| restartInterval); |
| |
| if (aborted) { |
| processWriteAborted(); |
| } else { |
| processImageComplete(); |
| } |
| |
| ios.flush(); |
| currentImage++; // After a successful write |
| } |
| |
| public void prepareWriteSequence(IIOMetadata streamMetadata) |
| throws IOException { |
| setThreadLock(); |
| try { |
| prepareWriteSequenceOnThread(streamMetadata); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private void prepareWriteSequenceOnThread(IIOMetadata streamMetadata) |
| throws IOException { |
| if (ios == null) { |
| throw new IllegalStateException("Output has not been set!"); |
| } |
| |
| /* |
| * from jpeg_metadata.html: |
| * If no stream metadata is supplied to |
| * <code>ImageWriter.prepareWriteSequence</code>, then no |
| * tables-only image is written. If stream metadata containing |
| * no tables is supplied to |
| * <code>ImageWriter.prepareWriteSequence</code>, then a tables-only |
| * image containing default visually lossless tables is written. |
| */ |
| if (streamMetadata != null) { |
| if (streamMetadata instanceof JPEGMetadata) { |
| // write a complete tables-only image at the beginning of |
| // the stream. |
| JPEGMetadata jmeta = (JPEGMetadata) streamMetadata; |
| if (jmeta.isStream == false) { |
| throw new IllegalArgumentException |
| ("Invalid stream metadata object."); |
| } |
| // Check that we are |
| // at the beginning of the stream, or can go there, and haven't |
| // written out the metadata already. |
| if (currentImage != 0) { |
| throw new IIOException |
| ("JPEG Stream metadata must precede all images"); |
| } |
| if (sequencePrepared == true) { |
| throw new IIOException("Stream metadata already written!"); |
| } |
| |
| // Set the tables |
| // If the metadata has no tables, use default tables. |
| streamQTables = collectQTablesFromMetadata(jmeta); |
| if (debug) { |
| System.out.println("after collecting from stream metadata, " |
| + "streamQTables.length is " |
| + streamQTables.length); |
| } |
| if (streamQTables == null) { |
| streamQTables = JPEG.getDefaultQTables(); |
| } |
| streamDCHuffmanTables = |
| collectHTablesFromMetadata(jmeta, true); |
| if (streamDCHuffmanTables == null) { |
| streamDCHuffmanTables = JPEG.getDefaultHuffmanTables(true); |
| } |
| streamACHuffmanTables = |
| collectHTablesFromMetadata(jmeta, false); |
| if (streamACHuffmanTables == null) { |
| streamACHuffmanTables = JPEG.getDefaultHuffmanTables(false); |
| } |
| |
| // Now write them out |
| writeTables(structPointer, |
| streamQTables, |
| streamDCHuffmanTables, |
| streamACHuffmanTables); |
| } else { |
| throw new IIOException("Stream metadata must be JPEG metadata"); |
| } |
| } |
| sequencePrepared = true; |
| } |
| |
| public void writeToSequence(IIOImage image, ImageWriteParam param) |
| throws IOException { |
| setThreadLock(); |
| try { |
| if (sequencePrepared == false) { |
| throw new IllegalStateException("sequencePrepared not called!"); |
| } |
| // In the case of JPEG this does nothing different from write |
| write(null, image, param); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public void endWriteSequence() throws IOException { |
| setThreadLock(); |
| try { |
| if (sequencePrepared == false) { |
| throw new IllegalStateException("sequencePrepared not called!"); |
| } |
| sequencePrepared = false; |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public synchronized void abort() { |
| setThreadLock(); |
| try { |
| super.abort(); |
| abortWrite(structPointer); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| private void resetInternalState() { |
| // reset C structures |
| resetWriter(structPointer); |
| |
| // reset local Java structures |
| srcRas = null; |
| raster = null; |
| convertTosRGB = false; |
| currentImage = 0; |
| numScans = 0; |
| metadata = null; |
| } |
| |
| public void reset() { |
| setThreadLock(); |
| try { |
| super.reset(); |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| public void dispose() { |
| setThreadLock(); |
| try { |
| if (structPointer != 0) { |
| disposerRecord.dispose(); |
| structPointer = 0; |
| } |
| } finally { |
| clearThreadLock(); |
| } |
| } |
| |
| ////////// End of public API |
| |
| ///////// Package-access API |
| |
| /** |
| * 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. |
| */ |
| void warningOccurred(int code) { |
| if ((code < 0) || (code > MAX_WARNING)){ |
| throw new InternalError("Invalid warning index"); |
| } |
| processWarningOccurred |
| (currentImage, |
| "com.sun.imageio.plugins.jpeg.JPEGImageWriterResources", |
| Integer.toString(code)); |
| } |
| |
| /** |
| * 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. |
| */ |
| void warningWithMessage(String msg) { |
| processWarningOccurred(currentImage, msg); |
| } |
| |
| void thumbnailStarted(int thumbnailIndex) { |
| processThumbnailStarted(currentImage, thumbnailIndex); |
| } |
| |
| // Provide access to protected superclass method |
| void thumbnailProgress(float percentageDone) { |
| processThumbnailProgress(percentageDone); |
| } |
| |
| // Provide access to protected superclass method |
| void thumbnailComplete() { |
| processThumbnailComplete(); |
| } |
| |
| ///////// End of Package-access API |
| |
| ///////// Private methods |
| |
| ///////// Metadata handling |
| |
| private void checkSOFBands(SOFMarkerSegment sof, int numBandsUsed) |
| throws IIOException { |
| // Does the metadata frame header, if any, match numBandsUsed? |
| if (sof != null) { |
| if (sof.componentSpecs.length != numBandsUsed) { |
| throw new IIOException |
| ("Metadata components != number of destination bands"); |
| } |
| } |
| } |
| |
| private void checkJFIF(JFIFMarkerSegment jfif, |
| ImageTypeSpecifier type, |
| boolean input) { |
| if (jfif != null) { |
| if (!JPEG.isJFIFcompliant(type, input)) { |
| ignoreJFIF = true; // type overrides metadata |
| warningOccurred(input |
| ? WARNING_IMAGE_METADATA_JFIF_MISMATCH |
| : WARNING_DEST_METADATA_JFIF_MISMATCH); |
| } |
| } |
| } |
| |
| private void checkAdobe(AdobeMarkerSegment adobe, |
| ImageTypeSpecifier type, |
| boolean input) { |
| if (adobe != null) { |
| int rightTransform = JPEG.transformForType(type, input); |
| if (adobe.transform != rightTransform) { |
| warningOccurred(input |
| ? WARNING_IMAGE_METADATA_ADOBE_MISMATCH |
| : WARNING_DEST_METADATA_ADOBE_MISMATCH); |
| if (rightTransform == JPEG.ADOBE_IMPOSSIBLE) { |
| ignoreAdobe = true; |
| } else { |
| newAdobeTransform = rightTransform; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Collect all the scan info from the given metadata, and |
| * organize it into the scan info array required by the |
| * IJG libray. It is much simpler to parse out this |
| * data in Java and then just copy the data in C. |
| */ |
| private int [] collectScans(JPEGMetadata metadata, |
| SOFMarkerSegment sof) { |
| List segments = new ArrayList(); |
| int SCAN_SIZE = 9; |
| int MAX_COMPS_PER_SCAN = 4; |
| for (Iterator iter = metadata.markerSequence.iterator(); |
| iter.hasNext();) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg instanceof SOSMarkerSegment) { |
| segments.add(seg); |
| } |
| } |
| int [] retval = null; |
| numScans = 0; |
| if (!segments.isEmpty()) { |
| numScans = segments.size(); |
| retval = new int [numScans*SCAN_SIZE]; |
| int index = 0; |
| for (int i = 0; i < numScans; i++) { |
| SOSMarkerSegment sos = (SOSMarkerSegment) segments.get(i); |
| retval[index++] = sos.componentSpecs.length; // num comps |
| for (int j = 0; j < MAX_COMPS_PER_SCAN; j++) { |
| if (j < sos.componentSpecs.length) { |
| int compSel = sos.componentSpecs[j].componentSelector; |
| for (int k = 0; k < sof.componentSpecs.length; k++) { |
| if (compSel == sof.componentSpecs[k].componentId) { |
| retval[index++] = k; |
| break; // out of for over sof comps |
| } |
| } |
| } else { |
| retval[index++] = 0; |
| } |
| } |
| retval[index++] = sos.startSpectralSelection; |
| retval[index++] = sos.endSpectralSelection; |
| retval[index++] = sos.approxHigh; |
| retval[index++] = sos.approxLow; |
| } |
| } |
| return retval; |
| } |
| |
| /** |
| * Finds all DQT marker segments and returns all the q |
| * tables as a single array of JPEGQTables. |
| */ |
| private JPEGQTable [] collectQTablesFromMetadata |
| (JPEGMetadata metadata) { |
| ArrayList tables = new ArrayList(); |
| Iterator iter = metadata.markerSequence.iterator(); |
| while (iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg instanceof DQTMarkerSegment) { |
| DQTMarkerSegment dqt = |
| (DQTMarkerSegment) seg; |
| tables.addAll(dqt.tables); |
| } |
| } |
| JPEGQTable [] retval = null; |
| if (tables.size() != 0) { |
| retval = new JPEGQTable[tables.size()]; |
| for (int i = 0; i < retval.length; i++) { |
| retval[i] = |
| new JPEGQTable(((DQTMarkerSegment.Qtable)tables.get(i)).data); |
| } |
| } |
| return retval; |
| } |
| |
| /** |
| * Finds all DHT marker segments and returns all the q |
| * tables as a single array of JPEGQTables. The metadata |
| * must not be for a progressive image, or an exception |
| * will be thrown when two Huffman tables with the same |
| * table id are encountered. |
| */ |
| private JPEGHuffmanTable[] collectHTablesFromMetadata |
| (JPEGMetadata metadata, boolean wantDC) throws IIOException { |
| ArrayList tables = new ArrayList(); |
| Iterator iter = metadata.markerSequence.iterator(); |
| while (iter.hasNext()) { |
| MarkerSegment seg = (MarkerSegment) iter.next(); |
| if (seg instanceof DHTMarkerSegment) { |
| DHTMarkerSegment dht = |
| (DHTMarkerSegment) seg; |
| for (int i = 0; i < dht.tables.size(); i++) { |
| DHTMarkerSegment.Htable htable = |
| (DHTMarkerSegment.Htable) dht.tables.get(i); |
| if (htable.tableClass == (wantDC ? 0 : 1)) { |
| tables.add(htable); |
| } |
| } |
| } |
| } |
| JPEGHuffmanTable [] retval = null; |
| if (tables.size() != 0) { |
| DHTMarkerSegment.Htable [] htables = |
| new DHTMarkerSegment.Htable[tables.size()]; |
| tables.toArray(htables); |
| retval = new JPEGHuffmanTable[tables.size()]; |
| for (int i = 0; i < retval.length; i++) { |
| retval[i] = null; |
| for (int j = 0; j < tables.size(); j++) { |
| if (htables[j].tableID == i) { |
| if (retval[i] != null) { |
| throw new IIOException("Metadata has duplicate Htables!"); |
| } |
| retval[i] = new JPEGHuffmanTable(htables[j].numCodes, |
| htables[j].values); |
| } |
| } |
| } |
| } |
| |
| return retval; |
| } |
| |
| /////////// End of metadata handling |
| |
| ////////////// ColorSpace conversion |
| |
| private int getSrcCSType(ImageTypeSpecifier type) { |
| return getSrcCSType(type.getColorModel()); |
| } |
| |
| private int getSrcCSType(RenderedImage rimage) { |
| return getSrcCSType(rimage.getColorModel()); |
| } |
| |
| private int getSrcCSType(ColorModel cm) { |
| int retval = JPEG.JCS_UNKNOWN; |
| if (cm != null) { |
| boolean alpha = cm.hasAlpha(); |
| ColorSpace cs = cm.getColorSpace(); |
| switch (cs.getType()) { |
| case ColorSpace.TYPE_GRAY: |
| retval = JPEG.JCS_GRAYSCALE; |
| break; |
| case ColorSpace.TYPE_RGB: |
| if (alpha) { |
| retval = JPEG.JCS_RGBA; |
| } else { |
| retval = JPEG.JCS_RGB; |
| } |
| break; |
| case ColorSpace.TYPE_YCbCr: |
| if (alpha) { |
| retval = JPEG.JCS_YCbCrA; |
| } else { |
| retval = JPEG.JCS_YCbCr; |
| } |
| break; |
| case ColorSpace.TYPE_3CLR: |
| if (cs == JPEG.JCS.getYCC()) { |
| if (alpha) { |
| retval = JPEG.JCS_YCCA; |
| } else { |
| retval = JPEG.JCS_YCC; |
| } |
| } |
| case ColorSpace.TYPE_CMYK: |
| retval = JPEG.JCS_CMYK; |
| break; |
| } |
| } |
| return retval; |
| } |
| |
| private int getDestCSType(ImageTypeSpecifier destType) { |
| ColorModel cm = destType.getColorModel(); |
| boolean alpha = cm.hasAlpha(); |
| ColorSpace cs = cm.getColorSpace(); |
| int retval = JPEG.JCS_UNKNOWN; |
| switch (cs.getType()) { |
| case ColorSpace.TYPE_GRAY: |
| retval = JPEG.JCS_GRAYSCALE; |
| break; |
| case ColorSpace.TYPE_RGB: |
| if (alpha) { |
| retval = JPEG.JCS_RGBA; |
| } else { |
| retval = JPEG.JCS_RGB; |
| } |
| break; |
| case ColorSpace.TYPE_YCbCr: |
| if (alpha) { |
| retval = JPEG.JCS_YCbCrA; |
| } else { |
| retval = JPEG.JCS_YCbCr; |
| } |
| break; |
| case ColorSpace.TYPE_3CLR: |
| if (cs == JPEG.JCS.getYCC()) { |
| if (alpha) { |
| retval = JPEG.JCS_YCCA; |
| } else { |
| retval = JPEG.JCS_YCC; |
| } |
| } |
| case ColorSpace.TYPE_CMYK: |
| retval = JPEG.JCS_CMYK; |
| break; |
| } |
| return retval; |
| } |
| |
| private int getDefaultDestCSType(ImageTypeSpecifier type) { |
| return getDefaultDestCSType(type.getColorModel()); |
| } |
| |
| private int getDefaultDestCSType(RenderedImage rimage) { |
| return getDefaultDestCSType(rimage.getColorModel()); |
| } |
| |
| private int getDefaultDestCSType(ColorModel cm) { |
| int retval = JPEG.JCS_UNKNOWN; |
| if (cm != null) { |
| boolean alpha = cm.hasAlpha(); |
| ColorSpace cs = cm.getColorSpace(); |
| switch (cs.getType()) { |
| case ColorSpace.TYPE_GRAY: |
| retval = JPEG.JCS_GRAYSCALE; |
| break; |
| case ColorSpace.TYPE_RGB: |
| if (alpha) { |
| retval = JPEG.JCS_YCbCrA; |
| } else { |
| retval = JPEG.JCS_YCbCr; |
| } |
| break; |
| case ColorSpace.TYPE_YCbCr: |
| if (alpha) { |
| retval = JPEG.JCS_YCbCrA; |
| } else { |
| retval = JPEG.JCS_YCbCr; |
| } |
| break; |
| case ColorSpace.TYPE_3CLR: |
| if (cs == JPEG.JCS.getYCC()) { |
| if (alpha) { |
| retval = JPEG.JCS_YCCA; |
| } else { |
| retval = JPEG.JCS_YCC; |
| } |
| } |
| case ColorSpace.TYPE_CMYK: |
| retval = JPEG.JCS_YCCK; |
| break; |
| } |
| } |
| return retval; |
| } |
| |
| private boolean isSubsampled(SOFMarkerSegment.ComponentSpec [] specs) { |
| int hsamp0 = specs[0].HsamplingFactor; |
| int vsamp0 = specs[0].VsamplingFactor; |
| for (int i = 1; i < specs.length; i++) { |
| if ((specs[i].HsamplingFactor != hsamp0) || |
| (specs[i].HsamplingFactor != hsamp0)) |
| return true; |
| } |
| return false; |
| } |
| |
| ////////////// End of ColorSpace conversion |
| |
| ////////////// Native methods and callbacks |
| |
| /** Sets up static native structures. */ |
| private static native void initWriterIDs(Class iosClass, |
| Class qTableClass, |
| Class huffClass); |
| |
| /** Sets up per-writer native structure and returns a pointer to it. */ |
| private native long initJPEGImageWriter(); |
| |
| /** Sets up native structures for output stream */ |
| private native void setDest(long structPointer, |
| ImageOutputStream ios); |
| |
| /** |
| * Returns <code>true</code> if the write was aborted. |
| */ |
| private native boolean writeImage(long structPointer, |
| byte [] data, |
| int inCsType, int outCsType, |
| int numBands, |
| int [] bandSizes, |
| int srcWidth, |
| int destWidth, int destHeight, |
| int stepX, int stepY, |
| JPEGQTable [] qtables, |
| boolean writeDQT, |
| JPEGHuffmanTable[] DCHuffmanTables, |
| JPEGHuffmanTable[] ACHuffmanTables, |
| boolean writeDHT, |
| boolean optimizeHuffman, |
| boolean progressive, |
| int numScans, |
| int [] scans, |
| int [] componentIds, |
| int [] HsamplingFactors, |
| int [] VsamplingFactors, |
| int [] QtableSelectors, |
| boolean haveMetadata, |
| int restartInterval); |
| |
| |
| /** |
| * Writes the metadata out when called by the native code, |
| * which will have already written the header to the stream |
| * and established the library state. This is simpler than |
| * breaking the write call in two. |
| */ |
| private void writeMetadata() throws IOException { |
| if (metadata == null) { |
| if (writeDefaultJFIF) { |
| JFIFMarkerSegment.writeDefaultJFIF(ios, |
| thumbnails, |
| iccProfile, |
| this); |
| } |
| if (writeAdobe) { |
| AdobeMarkerSegment.writeAdobeSegment(ios, newAdobeTransform); |
| } |
| } else { |
| metadata.writeToStream(ios, |
| ignoreJFIF, |
| forceJFIF, |
| thumbnails, |
| iccProfile, |
| ignoreAdobe, |
| newAdobeTransform, |
| this); |
| } |
| } |
| |
| /** |
| * Write out a tables-only image to the stream. |
| */ |
| private native void writeTables(long structPointer, |
| JPEGQTable [] qtables, |
| JPEGHuffmanTable[] DCHuffmanTables, |
| JPEGHuffmanTable[] ACHuffmanTables); |
| |
| /** |
| * Put the scanline y of the source ROI view Raster into the |
| * 1-line Raster for writing. This handles ROI and band |
| * rearrangements, and expands indexed images. Subsampling is |
| * done in the native code. |
| * This is called by the native code. |
| */ |
| private void grabPixels(int y) { |
| |
| Raster sourceLine = null; |
| if (indexed) { |
| sourceLine = srcRas.createChild(sourceXOffset, |
| sourceYOffset+y, |
| sourceWidth, 1, |
| 0, 0, |
| new int [] {0}); |
| // If the image has BITMASK transparency, we need to make sure |
| // it gets converted to 32-bit ARGB, because the JPEG encoder |
| // relies upon the full 8-bit alpha channel. |
| boolean forceARGB = |
| (indexCM.getTransparency() != Transparency.OPAQUE); |
| BufferedImage temp = indexCM.convertToIntDiscrete(sourceLine, |
| forceARGB); |
| sourceLine = temp.getRaster(); |
| } else { |
| sourceLine = srcRas.createChild(sourceXOffset, |
| sourceYOffset+y, |
| sourceWidth, 1, |
| 0, 0, |
| srcBands); |
| } |
| if (convertTosRGB) { |
| if (debug) { |
| System.out.println("Converting to sRGB"); |
| } |
| // The first time through, converted is null, so |
| // a new raster is allocated. It is then reused |
| // on subsequent lines. |
| converted = convertOp.filter(sourceLine, converted); |
| sourceLine = converted; |
| } |
| if (isAlphaPremultiplied) { |
| WritableRaster wr = sourceLine.createCompatibleWritableRaster(); |
| int[] data = null; |
| data = sourceLine.getPixels(sourceLine.getMinX(), sourceLine.getMinY(), |
| sourceLine.getWidth(), sourceLine.getHeight(), |
| data); |
| wr.setPixels(sourceLine.getMinX(), sourceLine.getMinY(), |
| sourceLine.getWidth(), sourceLine.getHeight(), |
| data); |
| srcCM.coerceData(wr, false); |
| sourceLine = wr.createChild(wr.getMinX(), wr.getMinY(), |
| wr.getWidth(), wr.getHeight(), |
| 0, 0, |
| srcBands); |
| } |
| raster.setRect(sourceLine); |
| if ((y > 7) && (y%8 == 0)) { // Every 8 scanlines |
| processImageProgress((float) y / (float) sourceHeight * 100.0F); |
| } |
| } |
| |
| /** Aborts the current write in the native code */ |
| private native void abortWrite(long structPointer); |
| |
| /** Resets native structures */ |
| private native void resetWriter(long structPointer); |
| |
| /** Releases native structures */ |
| private static native void disposeWriter(long structPointer); |
| |
| private static class JPEGWriterDisposerRecord implements DisposerRecord { |
| private long pData; |
| |
| public JPEGWriterDisposerRecord(long pData) { |
| this.pData = pData; |
| } |
| |
| public synchronized void dispose() { |
| if (pData != 0) { |
| disposeWriter(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; |
| } |
| } |
| } |