view dwt/internal/image/GIFFileFormat.d @ 13:3d9bbe0a83a0

FileFormats
author Frank Benoit <benoit@tionex.de>
date Sun, 06 Jan 2008 22:54:14 +0100
parents
children fc2b263b8a3f
line wrap: on
line source

/*******************************************************************************
 * Copyright (c) 2000, 2005 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
 *******************************************************************************/
module dwt.internal.image.GIFFileFormat;

public import dwt.internal.image.FileFormat;
public import dwt.graphics.PaletteData;
import dwt.internal.image.LEDataInputStream;
import dwt.internal.image.LZWCodec;
import dwt.graphics.RGB;
import dwt.SWT;
import dwt.graphics.ImageData;
import dwt.graphics.ImageLoaderEvent;
import dwt.graphics.ImageLoader;
import tango.core.Exception;

///FORTING_TYPE
class Image{}

final class GIFFileFormat : FileFormat {
	char[] signature;
	int screenWidth, screenHeight, backgroundPixel, bitsPerPixel, defaultDepth;
	int disposalMethod = 0;
	int delayTime = 0;
	int transparentPixel = -1;
	int repeatCount = 1;

	static final int GIF_APPLICATION_EXTENSION_BLOCK_ID = 0xFF;
	static final int GIF_GRAPHICS_CONTROL_BLOCK_ID = 0xF9;
	static final int GIF_PLAIN_TEXT_BLOCK_ID = 0x01;
	static final int GIF_COMMENT_BLOCK_ID = 0xFE;
	static final int GIF_EXTENSION_BLOCK_ID = 0x21;
	static final int GIF_IMAGE_BLOCK_ID = 0x2C;
	static final int GIF_TRAILER_ID = 0x3B;
	static final byte[] GIF89a = cast(byte[])"GIF89a";
	static final byte[] NETSCAPE2_0 = cast(byte[])"NETSCAPE2.0";

	/**
	 * Answer a palette containing numGrays
	 * shades of gray, ranging from black to white.
	 */
	static PaletteData grayRamp(int numGrays) {
		int n = numGrays - 1;
		RGB[] colors = new RGB[numGrays];
		for (int i = 0; i < numGrays; i++) {
			int intensity = cast(byte)((i * 3) * 256 / n);
			colors[i] = new RGB(intensity, intensity, intensity);
		}
		return new PaletteData(colors);
	}

	bool isFileFormat(LEDataInputStream stream) {
		try {
			byte[3] signature;
			stream.read(signature);
			stream.unread(signature);
			return signature == cast(byte[])"GIF"; //$NON-NLS-1$
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * Load the GIF image(s) stored in the input stream.
	 * Return an array of ImageData representing the image(s).
	 */
	ImageData[] loadFromByteStream() {
		byte[3] signatureBytes;
		byte[3] versionBytes;
		byte[7] block;
		try {
			inputStream.read(signatureBytes);
			signature = cast(char[])signatureBytes.dup;
			if (signature != "GIF") //$NON-NLS-1$
				SWT.error(SWT.ERROR_INVALID_IMAGE);

			inputStream.read(versionBytes);

			inputStream.read(block);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		screenWidth = (block[0] & 0xFF) | ((block[1] & 0xFF) << 8);
		loader.logicalScreenWidth = screenWidth;
		screenHeight = (block[2] & 0xFF) | ((block[3] & 0xFF) << 8);
		loader.logicalScreenHeight = screenHeight;
		byte bitField = block[4];
		backgroundPixel = block[5] & 0xFF;
		//aspect = block[6] & 0xFF;
		bitsPerPixel = ((bitField >> 4) & 0x07) + 1;
		defaultDepth = (bitField & 0x7) + 1;
		PaletteData palette = null;
		if ((bitField & 0x80) != 0) {
			// Global palette.
			//sorted = (bitField & 0x8) != 0;
			palette = readPalette(1 << defaultDepth);
		} else {
			// No global palette.
			//sorted = false;
			backgroundPixel = -1;
			defaultDepth = bitsPerPixel;
		}
		loader.backgroundPixel = backgroundPixel;

		getExtensions();
		int id = readID();
		ImageData[] images = new ImageData[0];
		while (id == GIF_IMAGE_BLOCK_ID) {
			ImageData image = readImageBlock(palette);
			if (loader.hasListeners()) {
				loader.notifyListeners(new ImageLoaderEvent(loader, image, 3, true));
			}
            images ~= image;
			try {
				/* Read the 0-byte terminator at the end of the image. */
				id = inputStream.read();
				if (id > 0) {
					/* We read the terminator earlier. */
                    byte[1] arr;
                    arr[0] = id;
					inputStream.unread( arr );
				}
			} catch (IOException e) {
				SWT.error(SWT.ERROR_IO, e);
			}
			getExtensions();
			id = readID();
		}
		return images;
	}

	/**
	 * Read and return the next block or extension identifier from the file.
	 */
	int readID() {
		try {
			return inputStream.read();
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		return -1;
	}

	/**
	 * Read extensions until an image descriptor appears.
	 * In the future, if we care about the extensions, they
	 * should be properly grouped with the image data before
	 * which they appeared. Right now, the interesting parts
	 * of some extensions are kept, but the rest is discarded.
	 * Throw an error if an error occurs.
	 */
	void getExtensions() {
		int id = readID();
		while (id != GIF_IMAGE_BLOCK_ID && id != GIF_TRAILER_ID && id > 0) {
			if (id == GIF_EXTENSION_BLOCK_ID) {
				readExtension();
			} else {
				SWT.error(SWT.ERROR_INVALID_IMAGE);
			}
			id = readID();
		}
		if (id == GIF_IMAGE_BLOCK_ID || id == GIF_TRAILER_ID) {
			try {
                byte[1] arr;
                arr[0] = id;
				inputStream.unread(arr);
			} catch (IOException e) {
				SWT.error(SWT.ERROR_IO, e);
			}
		}
	}

	/**
	 * Read a control extension.
	 * Return the extension block data.
	 */
	byte[] readExtension() {
		int extensionID = readID();
		if (extensionID == GIF_COMMENT_BLOCK_ID)
			return readCommentExtension();
		if (extensionID == GIF_PLAIN_TEXT_BLOCK_ID)
			return readPlainTextExtension();
		if (extensionID == GIF_GRAPHICS_CONTROL_BLOCK_ID)
			return readGraphicsControlExtension();
		if (extensionID == GIF_APPLICATION_EXTENSION_BLOCK_ID)
			return readApplicationExtension();
		// Otherwise, we don't recognize the block. If the
		// field size is correct, we can just skip over
		// the block contents.
		try {
			int extSize = inputStream.read();
			if (extSize < 0) {
				SWT.error(SWT.ERROR_INVALID_IMAGE);
			}
			byte[] ext = new byte[extSize];
			inputStream.read(ext, 0, extSize);
			return ext;
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * We have just read the Comment extension identifier
	 * from the input stream. Read in the rest of the comment
	 * and return it. GIF comment blocks are variable size.
	 */
	byte[] readCommentExtension() {
		try {
			byte[] comment = new byte[0];
			byte[] block = new byte[255];
			int size = inputStream.read();
			while ((size > 0) && (inputStream.read(block, 0, size) != -1)) {
                comment ~= block[ 0 .. size ];
				size = inputStream.read();
			}
			return comment;
		} catch (TracedException e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * We have just read the PlainText extension identifier
	 * from the input stream. Read in the plain text info and text,
	 * and return the text. GIF plain text blocks are variable size.
	 */
	byte[] readPlainTextExtension() {
		try {
			// Read size of block = 0x0C.
			inputStream.read();
			// Read the text information (x, y, width, height, colors).
			byte[] info = new byte[12];
			inputStream.read(info);
			// Read the text.
			byte[] text = new byte[0];
			byte[] block = new byte[255];
			int size = inputStream.read();
			while ((size > 0) && (inputStream.read(block, 0, size) != -1)) {
                text ~= block[ 0 .. size ];
				size = inputStream.read();
			}
			return text;
		} catch (TracedException e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * We have just read the GraphicsControl extension identifier
	 * from the input stream. Read in the control information, store
	 * it, and return it.
	 */
	byte[] readGraphicsControlExtension() {
		try {
			// Read size of block = 0x04.
			inputStream.read();
			// Read the control block.
			byte[] controlBlock = new byte[4];
			inputStream.read(controlBlock);
			byte bitField = controlBlock[0];
			// Store the user input field.
			//userInput = (bitField & 0x02) != 0;
			// Store the disposal method.
			disposalMethod = (bitField >> 2) & 0x07;
			// Store the delay time.
			delayTime = (controlBlock[1] & 0xFF) | ((controlBlock[2] & 0xFF) << 8);
			// Store the transparent color.
			if ((bitField & 0x01) != 0) {
				transparentPixel = controlBlock[3] & 0xFF;
			} else {
				transparentPixel = -1;
			}
			// Read block terminator.
			inputStream.read();
			return controlBlock;
		} catch (TracedException e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * We have just read the Application extension identifier
	 * from the input stream.  Read in the rest of the extension,
	 * look for and store 'number of repeats', and return the data.
	 */
	byte[] readApplicationExtension() {
		try {
			// Read size of block = 0x0B.
			inputStream.read();
			// Read application identifier.
			byte[] applicationBytes = new byte[8];
			inputStream.read(applicationBytes);
			char[] application = cast(char[])(applicationBytes.dup);
			// Read authentication code.
			byte[] authenticationBytes = new byte[3];
			inputStream.read(authenticationBytes);
			char[] authentication = cast(char[])(authenticationBytes.dup);
			// Read application data.
			byte[] data = new byte[0];
			byte[] block = new byte[255];
			int size = inputStream.read();
			while ((size > 0) && (inputStream.read(block, 0, size) != -1)) {
                data ~= block[ 0 .. size ];
				size = inputStream.read();
			}
			// Look for the NETSCAPE 'repeat count' field for an animated GIF.
			if (application=="NETSCAPE" && authentication=="2.0" && data[0] == 01) { //$NON-NLS-1$ //$NON-NLS-2$
				repeatCount = (data[1] & 0xFF) | ((data[2] & 0xFF) << 8);
				loader.repeatCount = repeatCount;
			}
			return data;
		} catch (TracedException e) {
			SWT.error(SWT.ERROR_IO, e);
			return null;
		}
	}

	/**
	 * Return a DeviceIndependentImage representing the
	 * image block at the current position in the input stream.
	 * Throw an error if an error occurs.
	 */
	ImageData readImageBlock(PaletteData defaultPalette) {
		int depth;
		PaletteData palette;
		byte[] block = new byte[9];
		try {
			inputStream.read(block);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		int left = (block[0] & 0xFF) | ((block[1] & 0xFF) << 8);
		int top = (block[2] & 0xFF) | ((block[3] & 0xFF) << 8);
		int width = (block[4] & 0xFF) | ((block[5] & 0xFF) << 8);
		int height = (block[6] & 0xFF) | ((block[7] & 0xFF) << 8);
		byte bitField = block[8];
		bool interlaced = (bitField & 0x40) != 0;
		//bool sorted = (bitField & 0x20) != 0;
		if ((bitField & 0x80) != 0) {
			// Local palette.
			depth = (bitField & 0x7) + 1;
			palette = readPalette(1 << depth);
		} else {
			// No local palette.
			depth = defaultDepth;
			palette = defaultPalette;
		}
		/* Work around: Ignore the case where a GIF specifies an
		 * invalid index for the transparent pixel that is larger
		 * than the number of entries in the palette. */
		if (transparentPixel > 1 << depth) {
			transparentPixel = -1;
		}
		// Promote depth to next highest supported value.
		if (!(depth == 1 || depth == 4 || depth == 8)) {
			if (depth < 4)
				depth = 4;
			else
				depth = 8;
		}
		if (palette == null) {
			palette = grayRamp(1 << depth);
		}
		int initialCodeSize = -1;
		try {
			initialCodeSize = inputStream.read();
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		if (initialCodeSize < 0) {
			SWT.error(SWT.ERROR_INVALID_IMAGE);
		}
		ImageData image = ImageData.internal_new(
			width,
			height,
			depth,
			palette,
			4,
			null,
			0,
			null,
			null,
			-1,
			transparentPixel,
			SWT.IMAGE_GIF,
			left,
			top,
			disposalMethod,
			delayTime);
		LZWCodec codec = new LZWCodec();
		codec.decode(inputStream, loader, image, interlaced, initialCodeSize);
		return image;
	}

	/**
	 * Read a palette from the input stream.
	 */
	PaletteData readPalette(int numColors) {
		byte[] bytes = new byte[numColors * 3];
		try {
			if (inputStream.read(bytes) != bytes.length)
				SWT.error(SWT.ERROR_INVALID_IMAGE);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
		RGB[] colors = new RGB[numColors];
		for (int i = 0; i < numColors; i++)
			colors[i] = new RGB(bytes[i*3] & 0xFF,
				bytes[i*3+1] & 0xFF, bytes[i*3+2] & 0xFF);
		return new PaletteData(colors);
	}

	void unloadIntoByteStream(ImageLoader loader) {

 		/* Step 1: Acquire GIF parameters. */
		ImageData[] data = loader.data;
		int frameCount = data.length;
		bool multi = frameCount > 1;
		ImageData firstImage = data[0];
		int logicalScreenWidth = multi ? loader.logicalScreenWidth : firstImage.width;
		int logicalScreenHeight = multi ? loader.logicalScreenHeight : firstImage.height;
		int backgroundPixel = loader.backgroundPixel;
		int depth = firstImage.depth;
		PaletteData palette = firstImage.palette;
		RGB[] colors = palette.getRGBs();
		short globalTable = 1;

		/* Step 2: Check for validity and global/local color map. */
		if (!(depth == 1 || depth == 4 || depth == 8)) {
			SWT.error(SWT.ERROR_UNSUPPORTED_DEPTH);
		}
		for (int i=0; i<frameCount; i++) {
			if (data[i].palette.isDirect) {
				SWT.error(SWT.ERROR_INVALID_IMAGE);
			}
			if (multi) {
				if (!(data[i].height <= logicalScreenHeight && data[i].width <= logicalScreenWidth && data[i].depth == depth)) {
					SWT.error(SWT.ERROR_INVALID_IMAGE);
				}
				if (globalTable == 1) {
					RGB rgbs[] = data[i].palette.getRGBs();
					if (rgbs.length != colors.length) {
						globalTable = 0;
					} else {
						for (int j=0; j<colors.length; j++) {
							if (!(rgbs[j].red == colors[j].red &&
								rgbs[j].green == colors[j].green &&
								rgbs[j].blue == colors[j].blue))
									globalTable = 0;
						}
					}
				}
			}
		}

		try {
 			/* Step 3: Write the GIF89a Header and Logical Screen Descriptor. */
			outputStream.write(GIF89a);
			int bitField = globalTable*128 + (depth-1)*16 + depth-1;
			outputStream.writeShort(cast(short)logicalScreenWidth);
			outputStream.writeShort(cast(short)logicalScreenHeight);
			outputStream.write(bitField);
			outputStream.write(backgroundPixel);
			outputStream.write(0); // Aspect ratio is 1:1
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}

		/* Step 4: Write Global Color Table if applicable. */
		if (globalTable == 1) {
			writePalette(palette, depth);
		}

		/* Step 5: Write Application Extension if applicable. */
		if (multi) {
			int repeatCount = loader.repeatCount;
			try {
				outputStream.write(GIF_EXTENSION_BLOCK_ID);
				outputStream.write(GIF_APPLICATION_EXTENSION_BLOCK_ID);
				outputStream.write(NETSCAPE2_0.length);
				outputStream.write(NETSCAPE2_0);
				outputStream.write(3); // Three bytes follow
				outputStream.write(1); // Extension type
				outputStream.writeShort(cast(short) repeatCount);
				outputStream.write(0); // Block terminator
			} catch (IOException e) {
				SWT.error(SWT.ERROR_IO, e);
			}
		}

		for (int frame=0; frame<frameCount; frame++) {

			/* Step 6: Write Graphics Control Block for each frame if applicable. */
			if (multi || data[frame].transparentPixel != -1) {
				writeGraphicsControlBlock(data[frame]);
			}

			/* Step 7: Write Image Header for each frame. */
			int x = data[frame].x;
			int y = data[frame].y;
			int width = data[frame].width;
			int height = data[frame].height;
			try {
				outputStream.write(GIF_IMAGE_BLOCK_ID);
				byte[] block = new byte[9];
				block[0] = cast(byte)(x & 0xFF);
				block[1] = cast(byte)((x >> 8) & 0xFF);
				block[2] = cast(byte)(y & 0xFF);
				block[3] = cast(byte)((y >> 8) & 0xFF);
				block[4] = cast(byte)(width & 0xFF);
				block[5] = cast(byte)((width >> 8) & 0xFF);
				block[6] = cast(byte)(height & 0xFF);
				block[7] = cast(byte)((height >> 8) & 0xFF);
				block[8] = cast(byte)(globalTable == 0 ? (depth-1) | 0x80 : 0x00);
				outputStream.write(block);
			} catch (IOException e) {
				SWT.error(SWT.ERROR_IO, e);
			}

			/* Step 8: Write Local Color Table for each frame if applicable. */
			if (globalTable == 0) {
				writePalette(data[frame].palette, depth);
			}

			/* Step 9: Write the actual data for each frame. */
			try {
				outputStream.write(depth); // Minimum LZW Code size
			} catch (IOException e) {
				SWT.error(SWT.ERROR_IO, e);
			}
			(new LZWCodec()).encode(outputStream, data[frame]);
		}

		/* Step 10: Write GIF terminator. */
		try {
			outputStream.write(0x3B);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
	}

	/**
	 * Write out a GraphicsControlBlock to describe
	 * the specified device independent image.
	 */
	void writeGraphicsControlBlock(ImageData image) {
		try {
			outputStream.write(GIF_EXTENSION_BLOCK_ID);
			outputStream.write(GIF_GRAPHICS_CONTROL_BLOCK_ID);
			byte[] gcBlock = new byte[4];
			gcBlock[0] = 0;
			gcBlock[1] = 0;
			gcBlock[2] = 0;
			gcBlock[3] = 0;
			if (image.transparentPixel != -1) {
				gcBlock[0] = cast(byte)0x01;
				gcBlock[3] = cast(byte)image.transparentPixel;
			}
			if (image.disposalMethod != 0) {
				gcBlock[0] |= cast(byte)((image.disposalMethod & 0x07) << 2);
			}
			if (image.delayTime != 0) {
				gcBlock[1] = cast(byte)(image.delayTime & 0xFF);
				gcBlock[2] = cast(byte)((image.delayTime >> 8) & 0xFF);
			}
			outputStream.write(cast(byte)gcBlock.length);
			outputStream.write(gcBlock);
			outputStream.write(0); // Block terminator
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
	}

	/**
	 * Write the specified palette to the output stream.
	 */
	void writePalette(PaletteData palette, int depth) {
		byte[] bytes = new byte[(1 << depth) * 3];
		int offset = 0;
		for (int i = 0; i < palette.colors.length; i++) {
			RGB color = palette.colors[i];
			bytes[offset] = cast(byte)color.red;
			bytes[offset + 1] = cast(byte)color.green;
			bytes[offset + 2] = cast(byte)color.blue;
			offset += 3;
		}
		try {
			outputStream.write(bytes);
		} catch (IOException e) {
			SWT.error(SWT.ERROR_IO, e);
		}
	}
}