| /******************************************************************************* |
| * Copyright (c) 2000, 2006 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.swt.internal.image; |
| |
| |
| import java.io.*; |
| import org.eclipse.swt.*; |
| import org.eclipse.swt.graphics.*; |
| import org.eclipse.swt.internal.*; |
| |
| final class PNGFileFormat extends FileFormat { |
| static final int SIGNATURE_LENGTH = 8; |
| static final int PRIME = 65521; |
| PngIhdrChunk headerChunk; |
| PngPlteChunk paletteChunk; |
| ImageData imageData; |
| byte[] data; |
| byte[] alphaPalette; |
| byte headerByte1; |
| byte headerByte2; |
| int adler; |
| |
| /** |
| * Skip over signature data. This has already been |
| * verified in isFileFormat(). |
| */ |
| 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 width = headerChunk.getWidth(), height = headerChunk.getHeight(); |
| if (width <= 0 || height <= 0) SWT.error(SWT.ERROR_INVALID_IMAGE); |
| int imageSize = getAlignedBytesPerRow() * height; |
| data = new byte[imageSize]; |
| imageData = ImageData.internal_new( |
| width, |
| height, |
| 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) throws IOException { |
| 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); |
| int transparentCount = 0, transparentPixel = -1; |
| for (int i = 0; i < alphaPalette.length; i++) { |
| if ((alphaPalette[i] & 0xFF) != 255) { |
| transparentCount++; |
| transparentPixel = i; |
| } |
| } |
| if (transparentCount == 0) { |
| alphaPalette = null; |
| } else if (transparentCount == 1 && alphaPalette[transparentPixel] == 0) { |
| alphaPalette = null; |
| imageData.transparentPixel = transparentPixel; |
| } |
| } |
| 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(ImageLoader loader) { |
| /* We do not currently support writing png. */ |
| SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT); |
| } |
| boolean isFileFormat(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) throws IOException { |
| InputStream stream = new PngInputStream(chunk, chunkReader); |
| boolean use3_2 = System.getProperty("org.eclipse.swt.internal.image.PNGFileFormat_3.2") != null; |
| InputStream inflaterStream = use3_2 ? null : Compatibility.newInflaterInputStream(stream); |
| if (inflaterStream != null) { |
| stream = new BufferedInputStream(inflaterStream); |
| } else { |
| stream = new PngDecodingDataStream(stream); |
| } |
| int interlaceMethod = headerChunk.getInterlaceMethod(); |
| if (interlaceMethod == PngIhdrChunk.INTERLACE_METHOD_NONE) { |
| readNonInterlacedImage(stream); |
| } else { |
| readInterlacedImage(stream); |
| } |
| /* |
| * InflaterInputStream does not consume all bytes in the stream |
| * when it is closed. This may leave unread IDAT chunks. The fix |
| * is to read all available bytes before closing it. |
| */ |
| while (stream.available() > 0) stream.read(); |
| stream.close(); |
| } |
| /** |
| * 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( |
| InputStream inputStream, |
| int rowInterval, |
| int columnInterval, |
| int startRow, |
| int startColumn, |
| int frameCount) throws IOException |
| { |
| 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 = (byte)inputStream.read(); |
| int read = 0; |
| while (read != bytesPerRow) { |
| read += inputStream.read(currentRow, read, bytesPerRow - read); |
| } |
| 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(InputStream inputStream) throws IOException { |
| readInterlaceFrame(inputStream, 8, 8, 0, 0, 0); |
| readInterlaceFrame(inputStream, 8, 8, 0, 4, 1); |
| readInterlaceFrame(inputStream, 8, 4, 4, 0, 2); |
| readInterlaceFrame(inputStream, 4, 4, 0, 2, 3); |
| readInterlaceFrame(inputStream, 4, 2, 2, 0, 4); |
| readInterlaceFrame(inputStream, 2, 2, 0, 1, 5); |
| readInterlaceFrame(inputStream, 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(InputStream inputStream) throws IOException { |
| 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; |
| int height = headerChunk.getHeight(); |
| for (int row = 0; row < height; row++) { |
| byte filterType = (byte)inputStream.read(); |
| int read = 0; |
| while (read != bytesPerRow) { |
| read += inputStream.read(currentRow, read, bytesPerRow - read); |
| } |
| 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; |
| } |
| } |
| |
| } |