view dwt/internal/image/PNGFileFormat.d @ 213:36f5cb12e1a2

Update to SWT 3.4M7
author Frank Benoit <benoit@tionex.de>
date Sat, 17 May 2008 17:34:28 +0200
parents 9a64a7781bab
children fd9c62a2998e
line wrap: on
line source

/*******************************************************************************
 * 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
 * Port to the D programming language:
 *     Frank Benoit <benoit@tionex.de>
 *******************************************************************************/
module dwt.internal.image.PNGFileFormat;


import dwt.DWT;
import dwt.graphics.ImageData;
import dwt.graphics.ImageLoaderEvent;
import dwt.graphics.ImageLoader;
import dwt.graphics.PaletteData;
import dwt.internal.Compatibility;
import dwt.internal.image.FileFormat;
import dwt.internal.image.PngIhdrChunk;
import dwt.internal.image.PngPlteChunk;
import dwt.internal.image.PngChunkReader;
import dwt.internal.image.PngChunk;
import dwt.internal.image.PngTrnsChunk;
import dwt.internal.image.PngIdatChunk;
import dwt.internal.image.PngEncoder;
import dwt.internal.image.PngInputStream;
import dwt.internal.image.PngDecodingDataStream;
import dwt.dwthelper.utils;

import dwt.dwthelper.BufferedInputStream;

import tango.core.Exception;

final class PNGFileFormat : 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()  {
    byte[] signature = new byte[SIGNATURE_LENGTH];
    inputStream.read(signature);
}
/**
 * Load the PNG image from the byte stream.
 */
override ImageData[] loadFromByteStream() {
    try {
        readSignature();
        PngChunkReader chunkReader = new PngChunkReader(inputStream);
        headerChunk = chunkReader.getIhdrChunk();
        int width = headerChunk.getWidth(), height = headerChunk.getHeight();
        if (width <= 0 || height <= 0) DWT.error(DWT.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,
            DWT.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 [imageData];
    } catch (IOException e) {
        DWT.error(DWT.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 = cast(PngPlteChunk) chunk;
                imageData.palette = paletteChunk.getPaletteData();
            }
            break;
        case PngChunk.CHUNK_tRNS:
            PngTrnsChunk trnsChunk = cast(PngTrnsChunk) chunk;
            if (trnsChunk.getTransparencyType(headerChunk) is
                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) !is 255) {
                        transparentCount++;
                        transparentPixel = i;
                    }
                }
                if (transparentCount is 0) {
                    alphaPalette = null;
                } else if (transparentCount is 1 && alphaPalette[transparentPixel] is 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.
                DWT.error(DWT.ERROR_INVALID_IMAGE);
            } else {
                // Read in the pixel data for the image. This should
                // go through all the image's IDAT chunks.
                PngIdatChunk dataChunk = cast(PngIdatChunk) chunk;
                readPixelData(dataChunk, chunkReader);
            }
            break;
        default:
            if (chunk.isCritical()) {
                // All critical chunks must be supported.
                DWT.error(DWT.ERROR_NOT_IMPLEMENTED);
            }
    }
}
override void unloadIntoByteStream(ImageLoader loader) {
    PngEncoder encoder = new PngEncoder(loader);
    encoder.encode(outputStream);
}
override bool isFileFormat(LEDataInputStream stream) {
    try {
        byte[] signature = new byte[SIGNATURE_LENGTH];
        stream.read(signature);
        stream.unread(signature);
        if ((signature[0] & 0xFF) !is 137) return false; //137
        if ((signature[1] & 0xFF) !is 80) return false; //P
        if ((signature[2] & 0xFF) !is 78) return false; //N
        if ((signature[3] & 0xFF) !is 71) return false; //G
        if ((signature[4] & 0xFF) !is 13) return false; //<RETURN>
        if ((signature[5] & 0xFF) !is 10) return false; //<LINEFEED>
        if ((signature[6] & 0xFF) !is 26) return false; //<CTRL/Z>
        if ((signature[7] & 0xFF) !is 10) return false; //<LINEFEED>        
        return true;
    } catch (Exception e) {
        return false;
    }
}
/**
 * DWT 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;
    }
}
/**
 * DWT 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 !is 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 DWT. If the image uses an unsupported
 * color type (either of the gray scale types) or bit
 * depth (16), convert the data to an DWT-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)  {
    InputStream stream = new PngInputStream(chunk, chunkReader);
    //TEMPORARY CODE
    //PORTING_FIXME
    bool use3_2 = true;//System.getProperty("dwt.internal.image.PNGFileFormat_3.2") !is null;
    InputStream inflaterStream = use3_2 ? null : Compatibility.newInflaterInputStream(stream);
    if (inflaterStream !is null) {
        stream = inflaterStream;
    } else {
        stream = new PngDecodingDataStream(stream);
    }
    int interlaceMethod = headerChunk.getInterlaceMethod();
    if (interlaceMethod is 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)
{
    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 = cast(byte)inputStream.read();
        int read = 0;
        while (read !is 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 is row1) ? row2 : row1;
        lastRow = (lastRow is row1) ? row2 : row1;
    }
    setImageDataValues(data, imageData);
    fireInterlacedFrameEvent(frameCount);
}
/**
 * Read the pixel data for an interlaced image from the
 * data stream.
 */
void readInterlacedImage(InputStream inputStream)  {
    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 = cast(ImageData) imageData.clone();
        bool finalFrame = frameCount is 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)  {
    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 = cast(byte)inputStream.read();
        int read = 0;
        while (read !is bytesPerRow) {
            read += inputStream.read(currentRow, read, bytesPerRow - read);
        }
        filterRow(currentRow, lastRow, filterType);
        System.arraycopy(currentRow, 0, data, dataOffset, bytesPerRow);
        dataOffset += alignedBytesPerRow;
        currentRow = (currentRow is row1) ? row2 : row1;
        lastRow = (lastRow is row1) ? row2 : row1;
    }
    setImageDataValues(data, imageData);
}
/**
 * DWT 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;
    }
}
/**
 * DWT 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] = cast(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] = cast(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] = cast(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] = cast(byte) ((currentValue + preductor) & 0xFF);
            }
            break;
        default:
    }
}

}