/*
* Copyright (c) 2000, 2015, 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.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.
/**
* 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 /** The output stream we write to */
private ImageOutputStream ios = null;
The Raster we will write from /** The Raster we will write from */
private Raster srcRas = null;
An intermediate Raster holding compressor-friendly data /** An intermediate Raster holding compressor-friendly data */
private WritableRaster raster = null;
Set to true if we are writing an image with an
indexed ColorModel
/**
* 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.
/**
* 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.
/**
* 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 /** 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. /** The referent to be registered with the Disposer. */
private Object disposerReferent = new Object();
The DisposerRecord that handles the actual disposal of this writer. /** 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 java.security.PrivilegedAction<Void>() {
public Void run() {
System.loadLibrary("jpeg");
return null;
}
});
initWriterIDs(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 {
cbLock.check();
super.setOutput(output); // validates output
resetInternalState();
ios = (ImageOutputStream) output; // so this will always work
// Set the native destination
setDest(structPointer);
} 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 {
cbLock.check();
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] <= 0 || bandSizes[i] > 8) {
throw new IIOException("Illegal band size: should be 0 < size <= 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
clearAbortRequest();
cbLock.lock();
try {
processImageStarted(currentImage);
} finally {
cbLock.unlock();
}
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);
cbLock.lock();
try {
if (aborted) {
processWriteAborted();
} else {
processImageComplete();
}
ios.flush();
} finally {
cbLock.unlock();
}
currentImage++; // After a successful write
}
@Override
public boolean canWriteSequence() {
return true;
}
public void prepareWriteSequence(IIOMetadata streamMetadata)
throws IOException {
setThreadLock();
try {
cbLock.check();
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 {
cbLock.check();
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 {
cbLock.check();
if (sequencePrepared == false) {
throw new IllegalStateException("sequencePrepared not called!");
}
sequencePrepared = false;
} finally {
clearThreadLock();
}
}
public synchronized void abort() {
setThreadLock();
try {
/**
* NB: we do not check the call back lock here, we allow to abort
* the reader any time.
*/
super.abort();
abortWrite(structPointer);
} finally {
clearThreadLock();
}
}
@Override
protected synchronized void clearAbortRequest() {
setThreadLock();
try {
cbLock.check();
if (abortRequested()) {
super.clearAbortRequest();
// reset C structures
resetWriter(structPointer);
// reset the native destination
setDest(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 {
cbLock.check();
super.reset();
} finally {
clearThreadLock();
}
}
public void dispose() {
setThreadLock();
try {
cbLock.check();
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.
/**
* 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) {
cbLock.lock();
try {
if ((code < 0) || (code > MAX_WARNING)){
throw new InternalError("Invalid warning index");
}
processWarningOccurred
(currentImage,
"com.sun.imageio.plugins.jpeg.JPEGImageWriterResources",
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.
*/
void warningWithMessage(String msg) {
cbLock.lock();
try {
processWarningOccurred(currentImage, msg);
} 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();
}
}
///////// 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.
/**
* 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.
/**
* 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.
/**
* 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. /** Sets up static native structures. */
private static native void initWriterIDs(Class qTableClass,
Class huffClass);
Sets up per-writer native structure and returns a pointer to it. /** Sets up per-writer native structure and returns a pointer to it. */
private native long initJPEGImageWriter();
Sets up native structures for output stream /** Sets up native structures for output stream */
private native void setDest(long structPointer);
Returns true
if the write was aborted.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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
cbLock.lock();
try {
processImageProgress((float) y / (float) sourceHeight * 100.0F);
} finally {
cbLock.unlock();
}
}
}
Aborts the current write in the native code /** Aborts the current write in the native code */
private native void abortWrite(long structPointer);
Resets native structures /** Resets native structures */
private native void resetWriter(long structPointer);
Releases native structures /** 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;
}
}
}
This method is called from native code in order to write encoder
output to the destination.
We block any attempt to change the writer state during this
method, in order to prevent a corruption of the native encoder
state.
/**
* This method is called from native code in order to write encoder
* output to the destination.
*
* We block any attempt to change the writer state during this
* method, in order to prevent a corruption of the native encoder
* state.
*/
private void writeOutputData(byte[] data, int offset, int len)
throws IOException
{
cbLock.lock();
try {
ios.write(data, offset, len);
} finally {
cbLock.unlock();
}
}
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 writer is not allowed");
}
}
private void lock() {
lockState = State.Locked;
}
private void unlock() {
lockState = State.Unlocked;
}
private static enum State {
Unlocked,
Locked
}
}
}