blob: 3f9843d00402d7e0a03102d863a169a936a62be9 [file] [log] [blame]
package org.eclipse.swt.internal.image;
/*
* Copyright (c) 2000, 2002 IBM Corp. All rights reserved.
* This file is made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*/
import java.io.*;
import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
public class PNGFileFormat extends FileFormat {
static final int SIGNATURE_LENGTH = 8;
PngDecodingDataStream decodingStream;
PngIhdrChunk headerChunk;
PngPlteChunk paletteChunk;
PngTrnsChunk trnsChunk;
ImageData imageData;
byte[] data;
byte[] alphaPalette;
/**
* Skip over signature data. This has already been
* verified in isPNGFile().
*/
void readSignature() throws IOException {
byte[] signature = new byte[SIGNATURE_LENGTH];
inputStream.read(signature);
}
/**
* Load the PNG image from the byte stream.
*/
ImageData[] loadFromByteStream() {
try {
readSignature();
PngChunkReader chunkReader = new PngChunkReader(inputStream);
headerChunk = chunkReader.getIhdrChunk();
int imageSize = getAlignedBytesPerRow() * headerChunk.getHeight();
data = new byte[imageSize];
imageData = ImageData.internal_new(
headerChunk.getWidth(),
headerChunk.getHeight(),
headerChunk.getSwtBitsPerPixel(),
new PaletteData(0, 0, 0),
4,
data,
0,
null,
null,
-1,
-1,
SWT.IMAGE_PNG,
0,
0,
0,
0);
if (headerChunk.usesDirectColor()) {
imageData.palette = headerChunk.getPaletteData();
};
// Read and process chunks until the IEND chunk is encountered.
while (chunkReader.hasMoreChunks()) {
readNextChunk(chunkReader);
}
return new ImageData[] {imageData};
} catch (IOException e) {
SWT.error(SWT.ERROR_INVALID_IMAGE);
return null;
}
}
/**
* Read and handle the next chunk of data from the
* PNG file.
*/
void readNextChunk(PngChunkReader chunkReader) {
PngChunk chunk = chunkReader.readNextChunk();
switch (chunk.getChunkType()) {
case PngChunk.CHUNK_IEND:
break;
case PngChunk.CHUNK_PLTE:
if (!headerChunk.usesDirectColor()) {
paletteChunk = (PngPlteChunk) chunk;
imageData.palette = paletteChunk.getPaletteData();
};
break;
case PngChunk.CHUNK_tRNS:
PngTrnsChunk trnsChunk = (PngTrnsChunk) chunk;
if (trnsChunk.getTransparencyType(headerChunk) ==
PngTrnsChunk.TRANSPARENCY_TYPE_PIXEL)
{
imageData.transparentPixel =
trnsChunk.getSwtTransparentPixel(headerChunk);
} else {
alphaPalette = trnsChunk.getAlphaValues(headerChunk, paletteChunk);
}
break;
case PngChunk.CHUNK_IDAT:
if (chunkReader.readPixelData()) {
// All IDAT chunks in an image file must be
// sequential. If the pixel data has already
// been read and another IDAT block is encountered,
// then this is an invalid image.
SWT.error(SWT.ERROR_INVALID_IMAGE);
} else {
// Read in the pixel data for the image. This should
// go through all the image's IDAT chunks.
PngIdatChunk dataChunk = (PngIdatChunk) chunk;
readPixelData(dataChunk, chunkReader);
}
break;
default:
if (chunk.isCritical()) {
// All critical chunks must be supported.
SWT.error(SWT.ERROR_NOT_IMPLEMENTED);
}
}
}
void unloadIntoByteStream(ImageData p1) {
SWT.error(SWT.ERROR_NOT_IMPLEMENTED);
}
/**
* Answer whether the specified input stream
* contains a PNG image.
*/
public static boolean isPNGFile(LEDataInputStream stream) {
try {
byte[] signature = new byte[SIGNATURE_LENGTH];
stream.read(signature);
stream.unread(signature);
if ((signature[0] & 0xFF) != 137) return false; //137
if ((signature[1] & 0xFF) != 80) return false; //P
if ((signature[2] & 0xFF) != 78) return false; //N
if ((signature[3] & 0xFF) != 71) return false; //G
if ((signature[4] & 0xFF) != 13) return false; //<RETURN>
if ((signature[5] & 0xFF) != 10) return false; //<LINEFEED>
if ((signature[6] & 0xFF) != 26) return false; //<CTRL/Z>
if ((signature[7] & 0xFF) != 10) return false; //<LINEFEED>
return true;
} catch (Exception e) {
return false;
}
}
/**
* SWT does not support 16-bit depths. If this image uses
* 16-bit depths, convert the data to an 8-bit depth.
*/
byte[] validateBitDepth(byte[] data) {
if (headerChunk.getBitDepth() > 8) {
byte[] result = new byte[data.length / 2];
compress16BitDepthTo8BitDepth(data, 0, result, 0, result.length);
return result;
} else {
return data;
}
}
/**
* SWT does not support greyscale as a color type. For
* plain grayscale, we create a palette. For Grayscale
* with Alpha, however, we need to convert the pixels
* to use RGB values.
* Note: This method assumes that the bit depth of the
* data has already been restricted to 8 or less.
*/
void setPixelData(byte[] data, ImageData imageData) {
switch (headerChunk.getColorType()) {
case PngIhdrChunk.COLOR_TYPE_GRAYSCALE_WITH_ALPHA:
{
int width = imageData.width;
int height = imageData.height;
int destBytesPerLine = imageData.bytesPerLine;
/*
* If the image uses 16-bit depth, it is converted
* to an 8-bit depth image.
*/
int srcBytesPerLine = getAlignedBytesPerRow();
if (headerChunk.getBitDepth() > 8) srcBytesPerLine /= 2;
byte[] rgbData = new byte[destBytesPerLine * height];
byte[] alphaData = new byte[width * height];
for (int y = 0; y < height; y++) {
int srcIndex = srcBytesPerLine * y;
int destIndex = destBytesPerLine * y;
int destAlphaIndex = width * y;
for (int x = 0; x < width; x++) {
byte grey = data[srcIndex];
byte alpha = data[srcIndex + 1];
rgbData[destIndex + 0] = grey;
rgbData[destIndex + 1] = grey;
rgbData[destIndex + 2] = grey;
alphaData[destAlphaIndex] = alpha;
srcIndex += 2;
destIndex += 3;
destAlphaIndex++;
}
}
imageData.data = rgbData;
imageData.alphaData = alphaData;
break;
}
case PngIhdrChunk.COLOR_TYPE_RGB_WITH_ALPHA:
{
int width = imageData.width;
int height = imageData.height;
int destBytesPerLine = imageData.bytesPerLine;
int srcBytesPerLine = getAlignedBytesPerRow();
/*
* If the image uses 16-bit depth, it is converted
* to an 8-bit depth image.
*/
if (headerChunk.getBitDepth() > 8) srcBytesPerLine /= 2;
byte[] rgbData = new byte[destBytesPerLine * height];
byte[] alphaData = new byte[width * height];
for (int y = 0; y < height; y++) {
int srcIndex = srcBytesPerLine * y;
int destIndex = destBytesPerLine * y;
int destAlphaIndex = width * y;
for (int x = 0; x < width; x++) {
rgbData[destIndex + 0] = data[srcIndex + 0];
rgbData[destIndex + 1] = data[srcIndex + 1];
rgbData[destIndex + 2] = data[srcIndex + 2];
alphaData[destAlphaIndex] = data[srcIndex + 3];
srcIndex += 4;
destIndex += 3;
destAlphaIndex++;
}
}
imageData.data = rgbData;
imageData.alphaData = alphaData;
break;
}
case PngIhdrChunk.COLOR_TYPE_RGB:
imageData.data = data;
break;
case PngIhdrChunk.COLOR_TYPE_PALETTE:
imageData.data = data;
if (alphaPalette != null) {
int size = imageData.width * imageData.height;
byte[] alphaData = new byte[size];
byte[] pixelData = new byte[size];
imageData.getPixels(0, 0, size, pixelData, 0);
for (int i = 0; i < pixelData.length; i++) {
alphaData[i] = alphaPalette[pixelData[i] & 0xFF];
}
imageData.alphaData = alphaData;
}
break;
default:
imageData.data = data;
break;
}
}
/**
* PNG supports some color types and bit depths that are
* unsupported by SWT. If the image uses an unsupported
* color type (either of the gray scale types) or bit
* depth (16), convert the data to an SWT-supported
* format. Then assign the data into the ImageData given.
*/
void setImageDataValues(byte[] data, ImageData imageData) {
byte[] result = validateBitDepth(data);
setPixelData(result, imageData);
}
/**
* Read the image data from the data stream. This must handle
* decoding the data, filtering, and interlacing.
*/
void readPixelData(PngIdatChunk chunk, PngChunkReader chunkReader) {
decodingStream = new PngDecodingDataStream(chunk, chunkReader);
int interlaceMethod = headerChunk.getInterlaceMethod();
if (interlaceMethod == PngIhdrChunk.INTERLACE_METHOD_NONE) {
readNonInterlacedImage();
} else {
readInterlacedImage();
}
decodingStream.assertImageDataAtEnd();
decodingStream.checkAdler();
}
/**
* Answer the number of bytes in a word-aligned row of pixel data.
*/
int getAlignedBytesPerRow() {
return ((getBytesPerRow(headerChunk.getWidth()) + 3) / 4) * 4;
}
/**
* Answer the number of bytes in each row of the image
* data. Each PNG row is byte-aligned, so images with bit
* depths less than a byte may have unused bits at the
* end of each row. The value of these bits is undefined.
*/
int getBytesPerRow() {
return getBytesPerRow(headerChunk.getWidth());
}
/**
* Answer the number of bytes needed to represent a pixel.
* This value depends on the image's color type and bit
* depth.
* Note that this method rounds up if an image's pixel size
* isn't byte-aligned.
*/
int getBytesPerPixel() {
int bitsPerPixel = headerChunk.getBitsPerPixel();
return (bitsPerPixel + 7) / 8;
}
/**
* Answer the number of bytes in a row of the given pixel
* width. Each row is byte-aligned, so images with bit
* depths less than a byte may have unused bits at the
* end of each row. The value of these bits is undefined.
*/
int getBytesPerRow(int rowWidthInPixels) {
int bitsPerPixel = headerChunk.getBitsPerPixel();
int bitsPerRow = bitsPerPixel * rowWidthInPixels;
int bitsPerByte = 8;
return (bitsPerRow + (bitsPerByte - 1)) / bitsPerByte;
}
/**
* 1. Read one of the seven frames of interlaced data.
* 2. Update the imageData.
* 3. Notify the image loader's listeners of the frame load.
*/
void readInterlaceFrame(
int rowInterval,
int columnInterval,
int startRow,
int startColumn,
int frameCount)
{
int width = headerChunk.getWidth();
int alignedBytesPerRow = getAlignedBytesPerRow();
int height = headerChunk.getHeight();
if (startRow >= height || startColumn >= width) return;
int pixelsPerRow = (width - startColumn + columnInterval - 1) / columnInterval;
int bytesPerRow = getBytesPerRow(pixelsPerRow);
byte[] row1 = new byte[bytesPerRow];
byte[] row2 = new byte[bytesPerRow];
byte[] currentRow = row1;
byte[] lastRow = row2;
for (int row = startRow; row < height; row += rowInterval) {
byte filterType = decodingStream.getNextDecodedByte();
for (int col = 0; col < bytesPerRow; col++) {
currentRow[col] = decodingStream.getNextDecodedByte();
}
filterRow(currentRow, lastRow, filterType);
if (headerChunk.getBitDepth() >= 8) {
int bytesPerPixel = getBytesPerPixel();
int dataOffset = (row * alignedBytesPerRow) + (startColumn * bytesPerPixel);
for (int rowOffset = 0; rowOffset < currentRow.length; rowOffset += bytesPerPixel) {
for (int byteOffset = 0; byteOffset < bytesPerPixel; byteOffset++) {
data[dataOffset + byteOffset] = currentRow[rowOffset + byteOffset];
}
dataOffset += (columnInterval * bytesPerPixel);
}
} else {
int bitsPerPixel = headerChunk.getBitDepth();
int pixelsPerByte = 8 / bitsPerPixel;
int column = startColumn;
int rowBase = row * alignedBytesPerRow;
int valueMask = 0;
for (int i = 0; i < bitsPerPixel; i++) {
valueMask <<= 1;
valueMask |= 1;
}
int maxShift = 8 - bitsPerPixel;
for (int byteOffset = 0; byteOffset < currentRow.length; byteOffset++) {
for (int bitOffset = maxShift; bitOffset >= 0; bitOffset -= bitsPerPixel) {
if (column < width) {
int dataOffset = rowBase + (column * bitsPerPixel / 8);
int value = (currentRow[byteOffset] >> bitOffset) & valueMask;
int dataShift = maxShift - (bitsPerPixel * (column % pixelsPerByte));
data[dataOffset] |= value << dataShift;
}
column += columnInterval;
}
}
}
currentRow = (currentRow == row1) ? row2 : row1;
lastRow = (lastRow == row1) ? row2 : row1;
}
setImageDataValues(data, imageData);
fireInterlacedFrameEvent(frameCount);
}
/**
* Read the pixel data for an interlaced image from the
* data stream.
*/
void readInterlacedImage() {
readInterlaceFrame(8, 8, 0, 0, 0);
readInterlaceFrame(8, 8, 0, 4, 1);
readInterlaceFrame(8, 4, 4, 0, 2);
readInterlaceFrame(4, 4, 0, 2, 3);
readInterlaceFrame(4, 2, 2, 0, 4);
readInterlaceFrame(2, 2, 0, 1, 5);
readInterlaceFrame(2, 1, 1, 0, 6);
}
/**
* Fire an event to let listeners know that an interlaced
* frame has been loaded.
* finalFrame should be true if the image has finished
* loading, false if there are more frames to come.
*/
void fireInterlacedFrameEvent(int frameCount) {
if (loader.hasListeners()) {
ImageData image = (ImageData) imageData.clone();
boolean finalFrame = frameCount == 6;
loader.notifyListeners(new ImageLoaderEvent(loader, image, frameCount, finalFrame));
}
}
/**
* Read the pixel data for a non-interlaced image from the
* data stream.
* Update the imageData to reflect the new data.
*/
void readNonInterlacedImage() {
int dataOffset = 0;
int alignedBytesPerRow = getAlignedBytesPerRow();
int bytesPerRow = getBytesPerRow();
byte[] row1 = new byte[bytesPerRow];
byte[] row2 = new byte[bytesPerRow];
byte[] currentRow = row1;
byte[] lastRow = row2;
for (int row = 0; row < headerChunk.getHeight(); row++) {
byte filterType = decodingStream.getNextDecodedByte();
for (int col = 0; col < bytesPerRow; col++) {
currentRow[col] = decodingStream.getNextDecodedByte();
}
filterRow(currentRow, lastRow, filterType);
System.arraycopy(currentRow, 0, data, dataOffset, bytesPerRow);
dataOffset += alignedBytesPerRow;
currentRow = (currentRow == row1) ? row2 : row1;
lastRow = (lastRow == row1) ? row2 : row1;
}
setImageDataValues(data, imageData);
}
/**
* SWT does not support 16-bit depth color formats.
* Convert the 16-bit data to 8-bit data.
* The correct way to do this is to multiply each
* 16 bit value by the value:
* (2^8 - 1) / (2^16 - 1).
* The fast way to do this is just to drop the low
* byte of the 16-bit value.
*/
static void compress16BitDepthTo8BitDepth(
byte[] source,
int sourceOffset,
byte[] destination,
int destinationOffset,
int numberOfValues)
{
//double multiplier = (Compatibility.pow2(8) - 1) / (Compatibility.pow2(16) - 1);
for (int i = 0; i < numberOfValues; i++) {
int sourceIndex = sourceOffset + (2 * i);
int destinationIndex = destinationOffset + i;
//int value = (source[sourceIndex] << 8) | source[sourceIndex + 1];
//byte compressedValue = (byte)(value * multiplier);
byte compressedValue = source[sourceIndex];
destination[destinationIndex] = compressedValue;
}
}
/**
* SWT does not support 16-bit depth color formats.
* Convert the 16-bit data to 8-bit data.
* The correct way to do this is to multiply each
* 16 bit value by the value:
* (2^8 - 1) / (2^16 - 1).
* The fast way to do this is just to drop the low
* byte of the 16-bit value.
*/
static int compress16BitDepthTo8BitDepth(int value) {
//double multiplier = (Compatibility.pow2(8) - 1) / (Compatibility.pow2(16) - 1);
//byte compressedValue = (byte)(value * multiplier);
return value >> 8;
}
/**
* PNG supports four filtering types. These types are applied
* per row of image data. This method unfilters the given row
* based on the filterType.
*/
void filterRow(byte[] row, byte[] previousRow, int filterType) {
int byteOffset = headerChunk.getFilterByteOffset();
switch (filterType) {
case PngIhdrChunk.FILTER_NONE:
break;
case PngIhdrChunk.FILTER_SUB:
for (int i = byteOffset; i < row.length; i++) {
int current = row[i] & 0xFF;
int left = row[i - byteOffset] & 0xFF;
row[i] = (byte)((current + left) & 0xFF);
}
break;
case PngIhdrChunk.FILTER_UP:
for (int i = 0; i < row.length; i++) {
int current = row[i] & 0xFF;
int above = previousRow[i] & 0xFF;
row[i] = (byte)((current + above) & 0xFF);
}
break;
case PngIhdrChunk.FILTER_AVERAGE:
for (int i = 0; i < row.length; i++) {
int left = (i < byteOffset) ? 0 : row[i - byteOffset] & 0xFF;
int above = previousRow[i] & 0xFF;
int current = row[i] & 0xFF;
row[i] = (byte)((current + ((left + above) / 2)) & 0xFF);
}
break;
case PngIhdrChunk.FILTER_PAETH:
for (int i = 0; i < row.length; i++) {
int left = (i < byteOffset) ? 0 : row[i - byteOffset] & 0xFF;
int aboveLeft = (i < byteOffset) ? 0 : previousRow[i - byteOffset] & 0xFF;
int above = previousRow[i] & 0xFF;
int a = Math.abs(above - aboveLeft);
int b = Math.abs(left - aboveLeft);
int c = Math.abs(left - aboveLeft + above - aboveLeft);
int preductor = 0;
if (a <= b && a <= c) {
preductor = left;
} else if (b <= c) {
preductor = above;
} else {
preductor = aboveLeft;
}
int currentValue = row[i] & 0xFF;
row[i] = (byte) ((currentValue + preductor) & 0xFF);
}
break;
}
}
}