|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
package com.sun.imageio.plugins.gif; |
|
|
|
import java.awt.Dimension; |
|
import java.awt.Rectangle; |
|
import java.awt.image.ColorModel; |
|
import java.awt.image.ComponentSampleModel; |
|
import java.awt.image.DataBufferByte; |
|
import java.awt.image.IndexColorModel; |
|
import java.awt.image.Raster; |
|
import java.awt.image.RenderedImage; |
|
import java.awt.image.SampleModel; |
|
import java.awt.image.WritableRaster; |
|
import java.io.IOException; |
|
import java.nio.ByteOrder; |
|
import java.util.Arrays; |
|
import java.util.Iterator; |
|
import java.util.Locale; |
|
import javax.imageio.IIOException; |
|
import javax.imageio.IIOImage; |
|
import javax.imageio.ImageTypeSpecifier; |
|
import javax.imageio.ImageWriteParam; |
|
import javax.imageio.ImageWriter; |
|
import javax.imageio.spi.ImageWriterSpi; |
|
import javax.imageio.metadata.IIOInvalidTreeException; |
|
import javax.imageio.metadata.IIOMetadata; |
|
import javax.imageio.metadata.IIOMetadataFormatImpl; |
|
import javax.imageio.metadata.IIOMetadataNode; |
|
import javax.imageio.stream.ImageOutputStream; |
|
import org.w3c.dom.Node; |
|
import org.w3c.dom.NodeList; |
|
import com.sun.imageio.plugins.common.LZWCompressor; |
|
import com.sun.imageio.plugins.common.PaletteBuilder; |
|
import sun.awt.image.ByteComponentRaster; |
|
|
|
public class GIFImageWriter extends ImageWriter { |
|
private static final boolean DEBUG = false; |
|
|
|
static final String STANDARD_METADATA_NAME = |
|
IIOMetadataFormatImpl.standardMetadataFormatName; |
|
|
|
static final String STREAM_METADATA_NAME = |
|
GIFWritableStreamMetadata.NATIVE_FORMAT_NAME; |
|
|
|
static final String IMAGE_METADATA_NAME = |
|
GIFWritableImageMetadata.NATIVE_FORMAT_NAME; |
|
|
|
|
|
|
|
*/ |
|
private ImageOutputStream stream = null; |
|
|
|
|
|
|
|
*/ |
|
private boolean isWritingSequence = false; |
|
|
|
|
|
|
|
*/ |
|
private boolean wroteSequenceHeader = false; |
|
|
|
|
|
|
|
*/ |
|
private GIFWritableStreamMetadata theStreamMetadata = null; |
|
|
|
|
|
|
|
*/ |
|
private int imageIndex = 0; |
|
|
|
|
|
|
|
|
|
*/ |
|
private static int getNumBits(int value) throws IOException { |
|
int numBits; |
|
switch(value) { |
|
case 2: |
|
numBits = 1; |
|
break; |
|
case 4: |
|
numBits = 2; |
|
break; |
|
case 8: |
|
numBits = 3; |
|
break; |
|
case 16: |
|
numBits = 4; |
|
break; |
|
case 32: |
|
numBits = 5; |
|
break; |
|
case 64: |
|
numBits = 6; |
|
break; |
|
case 128: |
|
numBits = 7; |
|
break; |
|
case 256: |
|
numBits = 8; |
|
break; |
|
default: |
|
throw new IOException("Bad palette length: "+value+"!"); |
|
} |
|
|
|
return numBits; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static void computeRegions(Rectangle sourceBounds, |
|
Dimension destSize, |
|
ImageWriteParam p) { |
|
ImageWriteParam param; |
|
int periodX = 1; |
|
int periodY = 1; |
|
if (p != null) { |
|
int[] sourceBands = p.getSourceBands(); |
|
if (sourceBands != null && |
|
(sourceBands.length != 1 || |
|
sourceBands[0] != 0)) { |
|
throw new IllegalArgumentException("Cannot sub-band image!"); |
|
} |
|
|
|
|
|
Rectangle sourceRegion = p.getSourceRegion(); |
|
if (sourceRegion != null) { |
|
|
|
sourceRegion = sourceRegion.intersection(sourceBounds); |
|
sourceBounds.setBounds(sourceRegion); |
|
} |
|
|
|
|
|
int gridX = p.getSubsamplingXOffset(); |
|
int gridY = p.getSubsamplingYOffset(); |
|
sourceBounds.x += gridX; |
|
sourceBounds.y += gridY; |
|
sourceBounds.width -= gridX; |
|
sourceBounds.height -= gridY; |
|
|
|
|
|
periodX = p.getSourceXSubsampling(); |
|
periodY = p.getSourceYSubsampling(); |
|
} |
|
|
|
|
|
destSize.setSize((sourceBounds.width + periodX - 1)/periodX, |
|
(sourceBounds.height + periodY - 1)/periodY); |
|
if (destSize.width <= 0 || destSize.height <= 0) { |
|
throw new IllegalArgumentException("Empty source region!"); |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static byte[] createColorTable(ColorModel colorModel, |
|
SampleModel sampleModel) |
|
{ |
|
byte[] colorTable; |
|
if (colorModel instanceof IndexColorModel) { |
|
IndexColorModel icm = (IndexColorModel)colorModel; |
|
int mapSize = icm.getMapSize(); |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
int ctSize = getGifPaletteSize(mapSize); |
|
|
|
byte[] reds = new byte[ctSize]; |
|
byte[] greens = new byte[ctSize]; |
|
byte[] blues = new byte[ctSize]; |
|
icm.getReds(reds); |
|
icm.getGreens(greens); |
|
icm.getBlues(blues); |
|
|
|
|
|
|
|
|
|
*/ |
|
for (int i = mapSize; i < ctSize; i++) { |
|
reds[i] = reds[0]; |
|
greens[i] = greens[0]; |
|
blues[i] = blues[0]; |
|
} |
|
|
|
colorTable = new byte[3*ctSize]; |
|
int idx = 0; |
|
for (int i = 0; i < ctSize; i++) { |
|
colorTable[idx++] = reds[i]; |
|
colorTable[idx++] = greens[i]; |
|
colorTable[idx++] = blues[i]; |
|
} |
|
} else if (sampleModel.getNumBands() == 1) { |
|
|
|
int numBits = sampleModel.getSampleSize()[0]; |
|
if (numBits > 8) { |
|
numBits = 8; |
|
} |
|
int colorTableLength = 3*(1 << numBits); |
|
colorTable = new byte[colorTableLength]; |
|
for (int i = 0; i < colorTableLength; i++) { |
|
colorTable[i] = (byte)(i/3); |
|
} |
|
} else { |
|
// We do not have enough information here |
|
|
|
colorTable = null; |
|
} |
|
|
|
return colorTable; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static int getGifPaletteSize(int x) { |
|
if (x <= 2) { |
|
return 2; |
|
} |
|
x = x - 1; |
|
x = x | (x >> 1); |
|
x = x | (x >> 2); |
|
x = x | (x >> 4); |
|
x = x | (x >> 8); |
|
x = x | (x >> 16); |
|
return x + 1; |
|
} |
|
|
|
|
|
|
|
public GIFImageWriter(GIFImageWriterSpi originatingProvider) { |
|
super(originatingProvider); |
|
if (DEBUG) { |
|
System.err.println("GIF Writer is created"); |
|
} |
|
} |
|
|
|
public boolean canWriteSequence() { |
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private void convertMetadata(String metadataFormatName, |
|
IIOMetadata inData, |
|
IIOMetadata outData) { |
|
String formatName = null; |
|
|
|
String nativeFormatName = inData.getNativeMetadataFormatName(); |
|
if (nativeFormatName != null && |
|
nativeFormatName.equals(metadataFormatName)) { |
|
formatName = metadataFormatName; |
|
} else { |
|
String[] extraFormatNames = inData.getExtraMetadataFormatNames(); |
|
|
|
if (extraFormatNames != null) { |
|
for (int i = 0; i < extraFormatNames.length; i++) { |
|
if (extraFormatNames[i].equals(metadataFormatName)) { |
|
formatName = metadataFormatName; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (formatName == null && |
|
inData.isStandardMetadataFormatSupported()) { |
|
formatName = STANDARD_METADATA_NAME; |
|
} |
|
|
|
if (formatName != null) { |
|
try { |
|
Node root = inData.getAsTree(formatName); |
|
outData.mergeTree(formatName, root); |
|
} catch(IIOInvalidTreeException e) { |
|
// ignore |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
public IIOMetadata convertStreamMetadata(IIOMetadata inData, |
|
ImageWriteParam param) { |
|
if (inData == null) { |
|
throw new IllegalArgumentException("inData == null!"); |
|
} |
|
|
|
IIOMetadata sm = getDefaultStreamMetadata(param); |
|
|
|
convertMetadata(STREAM_METADATA_NAME, inData, sm); |
|
|
|
return sm; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
public IIOMetadata convertImageMetadata(IIOMetadata inData, |
|
ImageTypeSpecifier imageType, |
|
ImageWriteParam param) { |
|
if (inData == null) { |
|
throw new IllegalArgumentException("inData == null!"); |
|
} |
|
if (imageType == null) { |
|
throw new IllegalArgumentException("imageType == null!"); |
|
} |
|
|
|
GIFWritableImageMetadata im = |
|
(GIFWritableImageMetadata)getDefaultImageMetadata(imageType, |
|
param); |
|
|
|
// Save interlace flag state. |
|
|
|
boolean isProgressive = im.interlaceFlag; |
|
|
|
convertMetadata(IMAGE_METADATA_NAME, inData, im); |
|
|
|
// Undo change to interlace flag if not MODE_COPY_FROM_METADATA. |
|
|
|
if (param != null && param.canWriteProgressive() && |
|
param.getProgressiveMode() != param.MODE_COPY_FROM_METADATA) { |
|
im.interlaceFlag = isProgressive; |
|
} |
|
|
|
return im; |
|
} |
|
|
|
public void endWriteSequence() throws IOException { |
|
if (stream == null) { |
|
throw new IllegalStateException("output == null!"); |
|
} |
|
if (!isWritingSequence) { |
|
throw new IllegalStateException("prepareWriteSequence() was not invoked!"); |
|
} |
|
writeTrailer(); |
|
resetLocal(); |
|
} |
|
|
|
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, |
|
ImageWriteParam param) { |
|
GIFWritableImageMetadata imageMetadata = |
|
new GIFWritableImageMetadata(); |
|
|
|
// Image dimensions |
|
|
|
SampleModel sampleModel = imageType.getSampleModel(); |
|
|
|
Rectangle sourceBounds = new Rectangle(sampleModel.getWidth(), |
|
sampleModel.getHeight()); |
|
Dimension destSize = new Dimension(); |
|
computeRegions(sourceBounds, destSize, param); |
|
|
|
imageMetadata.imageWidth = destSize.width; |
|
imageMetadata.imageHeight = destSize.height; |
|
|
|
// Interlacing |
|
|
|
if (param != null && param.canWriteProgressive() && |
|
param.getProgressiveMode() == ImageWriteParam.MODE_DISABLED) { |
|
imageMetadata.interlaceFlag = false; |
|
} else { |
|
imageMetadata.interlaceFlag = true; |
|
} |
|
|
|
// Local color table |
|
|
|
ColorModel colorModel = imageType.getColorModel(); |
|
|
|
imageMetadata.localColorTable = |
|
createColorTable(colorModel, sampleModel); |
|
|
|
// Transparency |
|
|
|
if (colorModel instanceof IndexColorModel) { |
|
int transparentIndex = |
|
((IndexColorModel)colorModel).getTransparentPixel(); |
|
if (transparentIndex != -1) { |
|
imageMetadata.transparentColorFlag = true; |
|
imageMetadata.transparentColorIndex = transparentIndex; |
|
} |
|
} |
|
|
|
return imageMetadata; |
|
} |
|
|
|
public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { |
|
GIFWritableStreamMetadata streamMetadata = |
|
new GIFWritableStreamMetadata(); |
|
streamMetadata.version = "89a"; |
|
return streamMetadata; |
|
} |
|
|
|
public ImageWriteParam getDefaultWriteParam() { |
|
return new GIFImageWriteParam(getLocale()); |
|
} |
|
|
|
public void prepareWriteSequence(IIOMetadata streamMetadata) |
|
throws IOException { |
|
|
|
if (stream == null) { |
|
throw new IllegalStateException("Output is not set."); |
|
} |
|
|
|
resetLocal(); |
|
|
|
|
|
if (streamMetadata == null) { |
|
this.theStreamMetadata = |
|
(GIFWritableStreamMetadata)getDefaultStreamMetadata(null); |
|
} else { |
|
this.theStreamMetadata = new GIFWritableStreamMetadata(); |
|
convertMetadata(STREAM_METADATA_NAME, streamMetadata, |
|
theStreamMetadata); |
|
} |
|
|
|
this.isWritingSequence = true; |
|
} |
|
|
|
public void reset() { |
|
super.reset(); |
|
resetLocal(); |
|
} |
|
|
|
|
|
|
|
*/ |
|
private void resetLocal() { |
|
this.isWritingSequence = false; |
|
this.wroteSequenceHeader = false; |
|
this.theStreamMetadata = null; |
|
this.imageIndex = 0; |
|
} |
|
|
|
public void setOutput(Object output) { |
|
super.setOutput(output); |
|
if (output != null) { |
|
if (!(output instanceof ImageOutputStream)) { |
|
throw new |
|
IllegalArgumentException("output is not an ImageOutputStream"); |
|
} |
|
this.stream = (ImageOutputStream)output; |
|
this.stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); |
|
} else { |
|
this.stream = null; |
|
} |
|
} |
|
|
|
public void write(IIOMetadata sm, |
|
IIOImage iioimage, |
|
ImageWriteParam p) throws IOException { |
|
if (stream == null) { |
|
throw new IllegalStateException("output == null!"); |
|
} |
|
if (iioimage == null) { |
|
throw new IllegalArgumentException("iioimage == null!"); |
|
} |
|
if (iioimage.hasRaster()) { |
|
throw new UnsupportedOperationException("canWriteRasters() == false!"); |
|
} |
|
|
|
resetLocal(); |
|
|
|
GIFWritableStreamMetadata streamMetadata; |
|
if (sm == null) { |
|
streamMetadata = |
|
(GIFWritableStreamMetadata)getDefaultStreamMetadata(p); |
|
} else { |
|
streamMetadata = |
|
(GIFWritableStreamMetadata)convertStreamMetadata(sm, p); |
|
} |
|
|
|
write(true, true, streamMetadata, iioimage, p); |
|
} |
|
|
|
public void writeToSequence(IIOImage image, ImageWriteParam param) |
|
throws IOException { |
|
if (stream == null) { |
|
throw new IllegalStateException("output == null!"); |
|
} |
|
if (image == null) { |
|
throw new IllegalArgumentException("image == null!"); |
|
} |
|
if (image.hasRaster()) { |
|
throw new UnsupportedOperationException("canWriteRasters() == false!"); |
|
} |
|
if (!isWritingSequence) { |
|
throw new IllegalStateException("prepareWriteSequence() was not invoked!"); |
|
} |
|
|
|
write(!wroteSequenceHeader, false, theStreamMetadata, |
|
image, param); |
|
|
|
if (!wroteSequenceHeader) { |
|
wroteSequenceHeader = true; |
|
} |
|
|
|
this.imageIndex++; |
|
} |
|
|
|
|
|
private boolean needToCreateIndex(RenderedImage image) { |
|
|
|
SampleModel sampleModel = image.getSampleModel(); |
|
ColorModel colorModel = image.getColorModel(); |
|
|
|
return sampleModel.getNumBands() != 1 || |
|
sampleModel.getSampleSize()[0] > 8 || |
|
colorModel.getComponentSize()[0] > 8; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private void write(boolean writeHeader, |
|
boolean writeTrailer, |
|
IIOMetadata sm, |
|
IIOImage iioimage, |
|
ImageWriteParam p) throws IOException { |
|
clearAbortRequest(); |
|
|
|
RenderedImage image = iioimage.getRenderedImage(); |
|
|
|
|
|
if (needToCreateIndex(image)) { |
|
image = PaletteBuilder.createIndexedImage(image); |
|
iioimage.setRenderedImage(image); |
|
} |
|
|
|
ColorModel colorModel = image.getColorModel(); |
|
SampleModel sampleModel = image.getSampleModel(); |
|
|
|
|
|
Rectangle sourceBounds = new Rectangle(image.getMinX(), |
|
image.getMinY(), |
|
image.getWidth(), |
|
image.getHeight()); |
|
Dimension destSize = new Dimension(); |
|
computeRegions(sourceBounds, destSize, p); |
|
|
|
|
|
GIFWritableImageMetadata imageMetadata = null; |
|
if (iioimage.getMetadata() != null) { |
|
imageMetadata = new GIFWritableImageMetadata(); |
|
convertMetadata(IMAGE_METADATA_NAME, iioimage.getMetadata(), |
|
imageMetadata); |
|
// Converted rgb image can use palette different from global. |
|
// In order to avoid color artefacts we want to be sure we use |
|
// appropriate palette. For this we initialize local color table |
|
// from current color and sample models. |
|
// At this point we can guarantee that local color table can be |
|
// build because image was already converted to indexed or |
|
|
|
if (imageMetadata.localColorTable == null) { |
|
imageMetadata.localColorTable = |
|
createColorTable(colorModel, sampleModel); |
|
|
|
// in case of indexed image we should take care of |
|
|
|
if (colorModel instanceof IndexColorModel) { |
|
IndexColorModel icm = |
|
(IndexColorModel)colorModel; |
|
int index = icm.getTransparentPixel(); |
|
imageMetadata.transparentColorFlag = (index != -1); |
|
if (imageMetadata.transparentColorFlag) { |
|
imageMetadata.transparentColorIndex = index; |
|
} |
|
/* NB: transparentColorFlag might have not beed reset for |
|
greyscale images but explicitly reseting it here |
|
is potentially not right thing to do until we have way |
|
to find whether current value was explicitly set by |
|
the user. |
|
*/ |
|
} |
|
} |
|
} |
|
|
|
|
|
byte[] globalColorTable = null; |
|
|
|
// Write the header (Signature+Logical Screen Descriptor+ |
|
|
|
if (writeHeader) { |
|
if (sm == null) { |
|
throw new IllegalArgumentException("Cannot write null header!"); |
|
} |
|
|
|
GIFWritableStreamMetadata streamMetadata = |
|
(GIFWritableStreamMetadata)sm; |
|
|
|
|
|
if (streamMetadata.version == null) { |
|
streamMetadata.version = "89a"; |
|
} |
|
|
|
|
|
if (streamMetadata.logicalScreenWidth == |
|
GIFMetadata.UNDEFINED_INTEGER_VALUE) |
|
{ |
|
streamMetadata.logicalScreenWidth = destSize.width; |
|
} |
|
|
|
if (streamMetadata.logicalScreenHeight == |
|
GIFMetadata.UNDEFINED_INTEGER_VALUE) |
|
{ |
|
streamMetadata.logicalScreenHeight = destSize.height; |
|
} |
|
|
|
if (streamMetadata.colorResolution == |
|
GIFMetadata.UNDEFINED_INTEGER_VALUE) |
|
{ |
|
streamMetadata.colorResolution = colorModel != null ? |
|
colorModel.getComponentSize()[0] : |
|
sampleModel.getSampleSize()[0]; |
|
} |
|
|
|
// Set the Global Color Table if not set, i.e., if not |
|
|
|
if (streamMetadata.globalColorTable == null) { |
|
if (isWritingSequence && imageMetadata != null && |
|
imageMetadata.localColorTable != null) { |
|
// Writing a sequence and a local color table was |
|
|
|
streamMetadata.globalColorTable = |
|
imageMetadata.localColorTable; |
|
} else if (imageMetadata == null || |
|
imageMetadata.localColorTable == null) { |
|
|
|
streamMetadata.globalColorTable = |
|
createColorTable(colorModel, sampleModel); |
|
} |
|
} |
|
|
|
// Set the Global Color Table. At this point it should be |
|
// A) the global color table provided in stream metadata, if any; |
|
// B) the local color table of the image metadata, if any, if |
|
// writing a sequence; |
|
// C) a table created on the basis of the first image ColorModel |
|
// and SampleModel if no local color table is available; or |
|
// D) null if none of the foregoing conditions obtain (which |
|
// should only be if a sequence is not being written and |
|
|
|
globalColorTable = streamMetadata.globalColorTable; |
|
|
|
|
|
int bitsPerPixel; |
|
if (globalColorTable != null) { |
|
bitsPerPixel = getNumBits(globalColorTable.length/3); |
|
} else if (imageMetadata != null && |
|
imageMetadata.localColorTable != null) { |
|
bitsPerPixel = |
|
getNumBits(imageMetadata.localColorTable.length/3); |
|
} else { |
|
bitsPerPixel = sampleModel.getSampleSize(0); |
|
} |
|
writeHeader(streamMetadata, bitsPerPixel); |
|
} else if (isWritingSequence) { |
|
globalColorTable = theStreamMetadata.globalColorTable; |
|
} else { |
|
throw new IllegalArgumentException("Must write header for single image!"); |
|
} |
|
|
|
|
|
writeImage(iioimage.getRenderedImage(), imageMetadata, p, |
|
globalColorTable, sourceBounds, destSize); |
|
|
|
|
|
if (writeTrailer) { |
|
writeTrailer(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private void writeImage(RenderedImage image, |
|
GIFWritableImageMetadata imageMetadata, |
|
ImageWriteParam param, byte[] globalColorTable, |
|
Rectangle sourceBounds, Dimension destSize) |
|
throws IOException { |
|
ColorModel colorModel = image.getColorModel(); |
|
SampleModel sampleModel = image.getSampleModel(); |
|
|
|
boolean writeGraphicsControlExtension; |
|
if (imageMetadata == null) { |
|
|
|
imageMetadata = (GIFWritableImageMetadata)getDefaultImageMetadata( |
|
new ImageTypeSpecifier(image), param); |
|
|
|
// Set GraphicControlExtension flag only if there is |
|
|
|
writeGraphicsControlExtension = imageMetadata.transparentColorFlag; |
|
} else { |
|
|
|
NodeList list = null; |
|
try { |
|
IIOMetadataNode root = (IIOMetadataNode) |
|
imageMetadata.getAsTree(IMAGE_METADATA_NAME); |
|
list = root.getElementsByTagName("GraphicControlExtension"); |
|
} catch(IllegalArgumentException iae) { |
|
// Should never happen. |
|
} |
|
|
|
|
|
writeGraphicsControlExtension = |
|
list != null && list.getLength() > 0; |
|
|
|
// If progressive mode is not MODE_COPY_FROM_METADATA, ensure |
|
|
|
if (param != null && param.canWriteProgressive()) { |
|
if (param.getProgressiveMode() == |
|
ImageWriteParam.MODE_DISABLED) { |
|
imageMetadata.interlaceFlag = false; |
|
} else if (param.getProgressiveMode() == |
|
ImageWriteParam.MODE_DEFAULT) { |
|
imageMetadata.interlaceFlag = true; |
|
} |
|
} |
|
} |
|
|
|
|
|
if (Arrays.equals(globalColorTable, imageMetadata.localColorTable)) { |
|
imageMetadata.localColorTable = null; |
|
} |
|
|
|
|
|
imageMetadata.imageWidth = destSize.width; |
|
imageMetadata.imageHeight = destSize.height; |
|
|
|
|
|
if (writeGraphicsControlExtension) { |
|
writeGraphicControlExtension(imageMetadata); |
|
} |
|
|
|
|
|
writePlainTextExtension(imageMetadata); |
|
writeApplicationExtension(imageMetadata); |
|
writeCommentExtension(imageMetadata); |
|
|
|
|
|
int bitsPerPixel = |
|
getNumBits(imageMetadata.localColorTable == null ? |
|
(globalColorTable == null ? |
|
sampleModel.getSampleSize(0) : |
|
globalColorTable.length/3) : |
|
imageMetadata.localColorTable.length/3); |
|
writeImageDescriptor(imageMetadata, bitsPerPixel); |
|
|
|
|
|
writeRasterData(image, sourceBounds, destSize, |
|
param, imageMetadata.interlaceFlag); |
|
} |
|
|
|
private void writeRows(RenderedImage image, LZWCompressor compressor, |
|
int sx, int sdx, int sy, int sdy, int sw, |
|
int dy, int ddy, int dw, int dh, |
|
int numRowsWritten, int progressReportRowPeriod) |
|
throws IOException { |
|
if (DEBUG) System.out.println("Writing unoptimized"); |
|
|
|
int[] sbuf = new int[sw]; |
|
byte[] dbuf = new byte[dw]; |
|
|
|
Raster raster = |
|
image.getNumXTiles() == 1 && image.getNumYTiles() == 1 ? |
|
image.getTile(0, 0) : image.getData(); |
|
for (int y = dy; y < dh; y += ddy) { |
|
if (numRowsWritten % progressReportRowPeriod == 0) { |
|
if (abortRequested()) { |
|
processWriteAborted(); |
|
return; |
|
} |
|
processImageProgress((numRowsWritten*100.0F)/dh); |
|
} |
|
|
|
raster.getSamples(sx, sy, sw, 1, 0, sbuf); |
|
for (int i = 0, j = 0; i < dw; i++, j += sdx) { |
|
dbuf[i] = (byte)sbuf[j]; |
|
} |
|
compressor.compress(dbuf, 0, dw); |
|
numRowsWritten++; |
|
sy += sdy; |
|
} |
|
} |
|
|
|
private void writeRowsOpt(byte[] data, int offset, int lineStride, |
|
LZWCompressor compressor, |
|
int dy, int ddy, int dw, int dh, |
|
int numRowsWritten, int progressReportRowPeriod) |
|
throws IOException { |
|
if (DEBUG) System.out.println("Writing optimized"); |
|
|
|
offset += dy*lineStride; |
|
lineStride *= ddy; |
|
for (int y = dy; y < dh; y += ddy) { |
|
if (numRowsWritten % progressReportRowPeriod == 0) { |
|
if (abortRequested()) { |
|
processWriteAborted(); |
|
return; |
|
} |
|
processImageProgress((numRowsWritten*100.0F)/dh); |
|
} |
|
|
|
compressor.compress(data, offset, dw); |
|
numRowsWritten++; |
|
offset += lineStride; |
|
} |
|
} |
|
|
|
private void writeRasterData(RenderedImage image, |
|
Rectangle sourceBounds, |
|
Dimension destSize, |
|
ImageWriteParam param, |
|
boolean interlaceFlag) throws IOException { |
|
|
|
int sourceXOffset = sourceBounds.x; |
|
int sourceYOffset = sourceBounds.y; |
|
int sourceWidth = sourceBounds.width; |
|
int sourceHeight = sourceBounds.height; |
|
|
|
int destWidth = destSize.width; |
|
int destHeight = destSize.height; |
|
|
|
int periodX; |
|
int periodY; |
|
if (param == null) { |
|
periodX = 1; |
|
periodY = 1; |
|
} else { |
|
periodX = param.getSourceXSubsampling(); |
|
periodY = param.getSourceYSubsampling(); |
|
} |
|
|
|
SampleModel sampleModel = image.getSampleModel(); |
|
int bitsPerPixel = sampleModel.getSampleSize()[0]; |
|
|
|
int initCodeSize = bitsPerPixel; |
|
if (initCodeSize == 1) { |
|
initCodeSize++; |
|
} |
|
stream.write(initCodeSize); |
|
|
|
LZWCompressor compressor = |
|
new LZWCompressor(stream, initCodeSize, false); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
boolean isOptimizedCase = |
|
periodX == 1 && periodY == 1 && |
|
image.getNumXTiles() == 1 && image.getNumYTiles() == 1 && |
|
sampleModel instanceof ComponentSampleModel && |
|
image.getTile(0, 0) instanceof ByteComponentRaster && |
|
image.getTile(0, 0).getDataBuffer() instanceof DataBufferByte; |
|
|
|
int numRowsWritten = 0; |
|
|
|
int progressReportRowPeriod = Math.max(destHeight/20, 1); |
|
|
|
processImageStarted(imageIndex); |
|
|
|
if (interlaceFlag) { |
|
if (DEBUG) System.out.println("Writing interlaced"); |
|
|
|
if (isOptimizedCase) { |
|
ByteComponentRaster tile = |
|
(ByteComponentRaster)image.getTile(0, 0); |
|
byte[] data = ((DataBufferByte)tile.getDataBuffer()).getData(); |
|
ComponentSampleModel csm = |
|
(ComponentSampleModel)tile.getSampleModel(); |
|
int offset = csm.getOffset(sourceXOffset, sourceYOffset, 0); |
|
|
|
offset += tile.getDataOffset(0); |
|
int lineStride = csm.getScanlineStride(); |
|
|
|
writeRowsOpt(data, offset, lineStride, compressor, |
|
0, 8, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
|
|
if (abortRequested()) { |
|
return; |
|
} |
|
|
|
numRowsWritten += destHeight/8; |
|
|
|
writeRowsOpt(data, offset, lineStride, compressor, |
|
4, 8, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
|
|
if (abortRequested()) { |
|
return; |
|
} |
|
|
|
numRowsWritten += (destHeight - 4)/8; |
|
|
|
writeRowsOpt(data, offset, lineStride, compressor, |
|
2, 4, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
|
|
if (abortRequested()) { |
|
return; |
|
} |
|
|
|
numRowsWritten += (destHeight - 2)/4; |
|
|
|
writeRowsOpt(data, offset, lineStride, compressor, |
|
1, 2, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
} else { |
|
writeRows(image, compressor, |
|
sourceXOffset, periodX, |
|
sourceYOffset, 8*periodY, |
|
sourceWidth, |
|
0, 8, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
|
|
if (abortRequested()) { |
|
return; |
|
} |
|
|
|
numRowsWritten += destHeight/8; |
|
|
|
writeRows(image, compressor, sourceXOffset, periodX, |
|
sourceYOffset + 4*periodY, 8*periodY, |
|
sourceWidth, |
|
4, 8, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
|
|
if (abortRequested()) { |
|
return; |
|
} |
|
|
|
numRowsWritten += (destHeight - 4)/8; |
|
|
|
writeRows(image, compressor, sourceXOffset, periodX, |
|
sourceYOffset + 2*periodY, 4*periodY, |
|
sourceWidth, |
|
2, 4, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
|
|
if (abortRequested()) { |
|
return; |
|
} |
|
|
|
numRowsWritten += (destHeight - 2)/4; |
|
|
|
writeRows(image, compressor, sourceXOffset, periodX, |
|
sourceYOffset + periodY, 2*periodY, |
|
sourceWidth, |
|
1, 2, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
} |
|
} else { |
|
if (DEBUG) System.out.println("Writing non-interlaced"); |
|
|
|
if (isOptimizedCase) { |
|
Raster tile = image.getTile(0, 0); |
|
byte[] data = ((DataBufferByte)tile.getDataBuffer()).getData(); |
|
ComponentSampleModel csm = |
|
(ComponentSampleModel)tile.getSampleModel(); |
|
int offset = csm.getOffset(sourceXOffset, sourceYOffset, 0); |
|
int lineStride = csm.getScanlineStride(); |
|
|
|
writeRowsOpt(data, offset, lineStride, compressor, |
|
0, 1, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
} else { |
|
writeRows(image, compressor, |
|
sourceXOffset, periodX, |
|
sourceYOffset, periodY, |
|
sourceWidth, |
|
0, 1, destWidth, destHeight, |
|
numRowsWritten, progressReportRowPeriod); |
|
} |
|
} |
|
|
|
if (abortRequested()) { |
|
return; |
|
} |
|
|
|
processImageProgress(100.0F); |
|
|
|
compressor.flush(); |
|
|
|
stream.write(0x00); |
|
|
|
processImageComplete(); |
|
} |
|
|
|
private void writeHeader(String version, |
|
int logicalScreenWidth, |
|
int logicalScreenHeight, |
|
int colorResolution, |
|
int pixelAspectRatio, |
|
int backgroundColorIndex, |
|
boolean sortFlag, |
|
int bitsPerPixel, |
|
byte[] globalColorTable) throws IOException { |
|
try { |
|
|
|
stream.writeBytes("GIF"+version); |
|
|
|
// Screen Descriptor |
|
|
|
stream.writeShort((short)logicalScreenWidth); |
|
|
|
|
|
stream.writeShort((short)logicalScreenHeight); |
|
|
|
// Global Color Table |
|
|
|
int packedFields = globalColorTable != null ? 0x80 : 0x00; |
|
packedFields |= ((colorResolution - 1) & 0x7) << 4; |
|
if (sortFlag) { |
|
packedFields |= 0x8; |
|
} |
|
packedFields |= (bitsPerPixel - 1); |
|
stream.write(packedFields); |
|
|
|
|
|
stream.write(backgroundColorIndex); |
|
|
|
|
|
stream.write(pixelAspectRatio); |
|
|
|
|
|
if (globalColorTable != null) { |
|
stream.write(globalColorTable); |
|
} |
|
} catch (IOException e) { |
|
throw new IIOException("I/O error writing header!", e); |
|
} |
|
} |
|
|
|
private void writeHeader(IIOMetadata streamMetadata, int bitsPerPixel) |
|
throws IOException { |
|
|
|
GIFWritableStreamMetadata sm; |
|
if (streamMetadata instanceof GIFWritableStreamMetadata) { |
|
sm = (GIFWritableStreamMetadata)streamMetadata; |
|
} else { |
|
sm = new GIFWritableStreamMetadata(); |
|
Node root = |
|
streamMetadata.getAsTree(STREAM_METADATA_NAME); |
|
sm.setFromTree(STREAM_METADATA_NAME, root); |
|
} |
|
|
|
writeHeader(sm.version, |
|
sm.logicalScreenWidth, |
|
sm.logicalScreenHeight, |
|
sm.colorResolution, |
|
sm.pixelAspectRatio, |
|
sm.backgroundColorIndex, |
|
sm.sortFlag, |
|
bitsPerPixel, |
|
sm.globalColorTable); |
|
} |
|
|
|
private void writeGraphicControlExtension(int disposalMethod, |
|
boolean userInputFlag, |
|
boolean transparentColorFlag, |
|
int delayTime, |
|
int transparentColorIndex) |
|
throws IOException { |
|
try { |
|
stream.write(0x21); |
|
stream.write(0xf9); |
|
|
|
stream.write(4); |
|
|
|
int packedFields = (disposalMethod & 0x3) << 2; |
|
if (userInputFlag) { |
|
packedFields |= 0x2; |
|
} |
|
if (transparentColorFlag) { |
|
packedFields |= 0x1; |
|
} |
|
stream.write(packedFields); |
|
|
|
stream.writeShort((short)delayTime); |
|
|
|
stream.write(transparentColorIndex); |
|
stream.write(0x00); |
|
} catch (IOException e) { |
|
throw new IIOException("I/O error writing Graphic Control Extension!", e); |
|
} |
|
} |
|
|
|
private void writeGraphicControlExtension(GIFWritableImageMetadata im) |
|
throws IOException { |
|
writeGraphicControlExtension(im.disposalMethod, |
|
im.userInputFlag, |
|
im.transparentColorFlag, |
|
im.delayTime, |
|
im.transparentColorIndex); |
|
} |
|
|
|
private void writeBlocks(byte[] data) throws IOException { |
|
if (data != null && data.length > 0) { |
|
int offset = 0; |
|
while (offset < data.length) { |
|
int len = Math.min(data.length - offset, 255); |
|
stream.write(len); |
|
stream.write(data, offset, len); |
|
offset += len; |
|
} |
|
} |
|
} |
|
|
|
private void writePlainTextExtension(GIFWritableImageMetadata im) |
|
throws IOException { |
|
if (im.hasPlainTextExtension) { |
|
try { |
|
stream.write(0x21); |
|
stream.write(0x1); |
|
|
|
stream.write(12); |
|
|
|
stream.writeShort(im.textGridLeft); |
|
stream.writeShort(im.textGridTop); |
|
stream.writeShort(im.textGridWidth); |
|
stream.writeShort(im.textGridHeight); |
|
stream.write(im.characterCellWidth); |
|
stream.write(im.characterCellHeight); |
|
stream.write(im.textForegroundColor); |
|
stream.write(im.textBackgroundColor); |
|
|
|
writeBlocks(im.text); |
|
|
|
stream.write(0x00); |
|
} catch (IOException e) { |
|
throw new IIOException("I/O error writing Plain Text Extension!", e); |
|
} |
|
} |
|
} |
|
|
|
private void writeApplicationExtension(GIFWritableImageMetadata im) |
|
throws IOException { |
|
if (im.applicationIDs != null) { |
|
Iterator iterIDs = im.applicationIDs.iterator(); |
|
Iterator iterCodes = im.authenticationCodes.iterator(); |
|
Iterator iterData = im.applicationData.iterator(); |
|
|
|
while (iterIDs.hasNext()) { |
|
try { |
|
stream.write(0x21); |
|
stream.write(0xff); |
|
|
|
stream.write(11); |
|
stream.write((byte[])iterIDs.next(), 0, 8); |
|
stream.write((byte[])iterCodes.next(), 0, 3); |
|
|
|
writeBlocks((byte[])iterData.next()); |
|
|
|
stream.write(0x00); |
|
} catch (IOException e) { |
|
throw new IIOException("I/O error writing Application Extension!", e); |
|
} |
|
} |
|
} |
|
} |
|
|
|
private void writeCommentExtension(GIFWritableImageMetadata im) |
|
throws IOException { |
|
if (im.comments != null) { |
|
try { |
|
Iterator iter = im.comments.iterator(); |
|
while (iter.hasNext()) { |
|
stream.write(0x21); |
|
stream.write(0xfe); |
|
writeBlocks((byte[])iter.next()); |
|
stream.write(0x00); |
|
} |
|
} catch (IOException e) { |
|
throw new IIOException("I/O error writing Comment Extension!", e); |
|
} |
|
} |
|
} |
|
|
|
private void writeImageDescriptor(int imageLeftPosition, |
|
int imageTopPosition, |
|
int imageWidth, |
|
int imageHeight, |
|
boolean interlaceFlag, |
|
boolean sortFlag, |
|
int bitsPerPixel, |
|
byte[] localColorTable) |
|
throws IOException { |
|
|
|
try { |
|
stream.write(0x2c); |
|
|
|
stream.writeShort((short)imageLeftPosition); |
|
stream.writeShort((short)imageTopPosition); |
|
stream.writeShort((short)imageWidth); |
|
stream.writeShort((short)imageHeight); |
|
|
|
int packedFields = localColorTable != null ? 0x80 : 0x00; |
|
if (interlaceFlag) { |
|
packedFields |= 0x40; |
|
} |
|
if (sortFlag) { |
|
packedFields |= 0x8; |
|
} |
|
packedFields |= (bitsPerPixel - 1); |
|
stream.write(packedFields); |
|
|
|
if (localColorTable != null) { |
|
stream.write(localColorTable); |
|
} |
|
} catch (IOException e) { |
|
throw new IIOException("I/O error writing Image Descriptor!", e); |
|
} |
|
} |
|
|
|
private void writeImageDescriptor(GIFWritableImageMetadata imageMetadata, |
|
int bitsPerPixel) |
|
throws IOException { |
|
|
|
writeImageDescriptor(imageMetadata.imageLeftPosition, |
|
imageMetadata.imageTopPosition, |
|
imageMetadata.imageWidth, |
|
imageMetadata.imageHeight, |
|
imageMetadata.interlaceFlag, |
|
imageMetadata.sortFlag, |
|
bitsPerPixel, |
|
imageMetadata.localColorTable); |
|
} |
|
|
|
private void writeTrailer() throws IOException { |
|
stream.write(0x3b); |
|
} |
|
} |
|
|
|
class GIFImageWriteParam extends ImageWriteParam { |
|
GIFImageWriteParam(Locale locale) { |
|
super(locale); |
|
this.canWriteCompressed = true; |
|
this.canWriteProgressive = true; |
|
this.compressionTypes = new String[] {"LZW", "lzw"}; |
|
this.compressionType = compressionTypes[0]; |
|
} |
|
|
|
public void setCompressionMode(int mode) { |
|
if (mode == MODE_DISABLED) { |
|
throw new UnsupportedOperationException("MODE_DISABLED is not supported."); |
|
} |
|
super.setCompressionMode(mode); |
|
} |
|
} |