A GIF Image Viewer in JavaScript

The JavaScript code below, when run, will present a file upload button, which will allow you to select a .GIF file, upload it, and display it on the web page.  To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit http://thiscouldbebetter.neocities.org/gifviewer.html.

You’d never actually need a JavaScript GIF viewer, of course, because any web browser that implements JavaScript already knows how to load and display GIFs just fine.  However, if you, like me, are interested in knowing more about the structure of a GIF file, and how it works at the bits-and-bytes level, this post is for you.

My original intent was this code was to use it as a basis for an online animated GIF editor.  Because, you know, there aren’t nearly enough animated GIFs on the internet.   But at the moment, it can only load static images, and there are some additional bugs besides.  One big one is that it doesn’t seem to understand how to handle black pixels, at all.  For example, if you drew a rainbow on a white background in Microsoft Paint, drew a single black line across it halfway down, and saved it as a GIF, and opened it with this code, nothing below that line would look even remotely right.  Something to do with my decompression algorithm, I guess.  If anyone out there can figure out how to fix it, I’d appreciate the help.

UPDATE 2018/02/15 – I have updated this code, making some changes to the LZW compressor/decompressor and its supporting bit- and byte-handling classes. The new version seems to avoid the problem with black pixels described above in the initial post.

UPDATE 2018/02/16 – I have updated this code again, so that the loaded image file can be converted back to bytes and then saved to file.

UPDATE 2018/02/17 – I have updated this code again, to support files of type “GIF87a”, and to correct previous misunderstandings about how color tables are parsed.


<html>
<body>

<div id="divUI">

	<label>GIF File to Load:</label>
	<input type="file" onchange="inputFileToLoad_Changed(event);"></input>
	<div id="divImage"></div>
	<button onclick="buttonSave_Clicked();">Save</button>

</div>

<script type="text/javascript">

// ui events

function buttonSave_Clicked()
{
	var imageFileGIF = Session.Instance.imageFileGIF;
	if (imageFileGIF == null)
	{
		alert("No file specified!");
	}
	else
	{
		var bytesToSave = imageFileGIF.toBytes();

		var numberOfBytes = bytesToSave.length;
		var bytesAsArrayBuffer = new ArrayBuffer(numberOfBytes);
		var bytesAsUIntArray = new Uint8Array(bytesAsArrayBuffer);
		for (var i = 0; i < numberOfBytes; i++)
		{
			bytesAsUIntArray[i] = bytesToSave[i];
		}

		var bytesAsBlob = new Blob
		(
			[ bytesAsArrayBuffer ],
			{type:"application/type"}
		);

		var downloadLink = document.createElement("a");
		downloadLink.href = URL.createObjectURL(bytesAsBlob);
		downloadLink.download = "Image.gif";
		downloadLink.click();
	}
}

function inputFileToLoad_Changed(event)
{
	var fileToLoad = event.target.files[0];
	if (fileToLoad != null)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event2)
		{
			var fileAsBinaryString = event2.target.result;
			var fileAsBytes = [];
			for (var i = 0; i < fileAsBinaryString.length; i++)
			{
				fileAsBytes.push(fileAsBinaryString.charCodeAt(i));
			}
			var imageFileGIF = ImageFileGIF.fromBytes(fileAsBytes);
			Session.Instance.imageFileGIF(imageFileGIF);
		}
		fileReader.readAsBinaryString(fileToLoad);
	}
}

// classes

function BitStream(bytes)
{
	if (bytes == null)
	{
		bytes = [];
	}

	this.bytes = bytes;
	this.byteOffset = 0;
	this.bitOffsetWithinByte = 0;
	this.byteCurrent = 0;
}
{
	// constants

	BitStream.BitsPerByte = 8;
	BitStream.NaturalLogarithmOf2 = Math.log(2);

	// instance methods

	BitStream.prototype.close = function()
	{
		if (this.bitOffsetWithinByte > 0)
		{
			this.bytes.push(this.byteCurrent);
		}
	}

	BitStream.prototype.hasMoreBits = function()
	{
		var returnValue = (this.byteOffset < this.bytes.length - 1 || this.bitOffsetWithinByte < 8);
		return returnValue;
	}

	BitStream.prototype.readBitBE = function()
	{
		var byteCurrent = this.bytes[this.byteOffset];
		var bitOffsetReversed = 8 - this.bitOffsetWithinByte - 1;
		var returnValue = (byteCurrent >> bitOffsetReversed) & 1;

		this.bitOffsetWithinByte++;
		if (this.bitOffsetWithinByte >= 8)
		{
			this.bitOffsetWithinByte = 0;
			this.byteOffset++;
		}

		return returnValue;
	}

	BitStream.prototype.readBitLE = function()
	{
		var byteCurrent = this.bytes[this.byteOffset];
		var returnValue = (byteCurrent >> this.bitOffsetWithinByte) & 1;

		this.bitOffsetWithinByte++;
		if (this.bitOffsetWithinByte >= 8)
		{
			this.bitOffsetWithinByte = 0;
			this.byteOffset++;
		}

		return returnValue;
	}

	BitStream.prototype.readIntegerLE = function(numberOfBitsInInteger)
	{
		var returnValue = 0;

		for (var i = 0; i < numberOfBitsInInteger; i++)
		{
			var bitRead = this.readBitLE();
			var bitShifted = bitRead << i;
			returnValue |= bitShifted;
		}

		return returnValue;
	}

	BitStream.prototype.writeBitBE = function(bitToWrite)
	{
		var bitOffsetReversed = 8 - this.bitOffsetWithinByte - 1;
		var bitShifted = (bitToWrite << bitOffsetReversed); // todo
		this.byteCurrent |= bitShifted;

		this.bitOffsetWithinByte++;

		if (this.bitOffsetWithinByte >= BitStream.BitsPerByte)
		{
			this.bytes.push(this.byteCurrent);
			this.byteOffset++;
			this.bitOffsetWithinByte = 0;
			this.byteCurrent = 0;
		}
	}

	BitStream.prototype.writeBitLE = function(bitToWrite)
	{
		var bitShifted = (bitToWrite << this.bitOffsetWithinByte); // todo
		this.byteCurrent |= bitShifted;

		this.bitOffsetWithinByte++;

		if (this.bitOffsetWithinByte >= BitStream.BitsPerByte)
		{
			this.bytes.push(this.byteCurrent);
			this.byteOffset++;
			this.bitOffsetWithinByte = 0;
			this.byteCurrent = 0;
		}
	}

	BitStream.prototype.writeNumber = function(numberToWrite, numberOfBitsToUse)
	{
		for (var b = 0; b < numberOfBitsToUse; b++)
		{
			var bitValue = (numberToWrite >> b) & 1;
			this.writeBitLE(bitValue); // todo
		}
	}
}

function ByteStream(bytes)
{
	this.bytes = bytes;
	this.byteOffset = 0;
}
{
	ByteStream.prototype.hasMoreBytes = function()
	{
		return (this.byteOffset < this.bytes.length);
	}

	ByteStream.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteOffset];
		this.byteOffset++;
		return returnValue;
	}

	ByteStream.prototype.readBytes = function(numberOfBytesToRead)
	{
		var returnValues = [];

		for (var i = 0; i < numberOfBytesToRead; i++)
		{
			var byte = this.readByte();
			returnValues.push(byte);
		}

		return returnValues;
	}

	ByteStream.prototype.readInteger = function(numberOfBytesToRead)
	{
		var returnValue = 0;

		for (var i = 0; i < numberOfBytesToRead; i++)
		{
			var byte = this.readByte();
			returnValue |= byte << (8 * i); // little-endian
		}

		return returnValue;
	}

	ByteStream.prototype.readString = function(lengthOfString)
	{
		var returnValue = "";

		var bytes = this.readBytes(lengthOfString);

		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			returnValue += String.fromCharCode(byte);
		}

		return returnValue;
	}

	ByteStream.prototype.writeByte = function(byteToWrite)
	{
		this.bytes.push(byteToWrite);
		this.byteOffset++;
	}

	ByteStream.prototype.writeBytes = function(bytesToWrite)
	{
		for (var i = 0; i < bytesToWrite.length; i++)
		{
			this.writeByte(bytesToWrite[i]);
		}
	}

	ByteStream.prototype.writeInteger = function(value, numberOfBytesToWrite)
	{
		for (var i = 0; i < numberOfBytesToWrite; i++)
		{
			var byte = (value >> (8 * i)) & 0xff; // little-endian
			this.writeByte(byte);
		}
	}

	ByteStream.prototype.writeString = function(stringToWrite)
	{
		for (var i = 0; i < stringToWrite.length; i++)
		{
			this.writeByte(stringToWrite.charCodeAt(i));
		}
	}
}

function Color(componentsRGB)
{
	this.componentsRGB = componentsRGB;
}

function CompressorLZW()
{
	// do nothing
}
{
	// constants

	CompressorLZW.SymbolForBitWidthIncrease = 256;
	CompressorLZW.SymbolForBitStreamEnd = CompressorLZW.SymbolForBitWidthIncrease + 1;

	// instance methods

	CompressorLZW.prototype.compressBytes = function(bytesToCompress)
	{
		var bitStream = new BitStream();

		// Adapted from pseudocode found at the URL:
		// http://oldwww.rasip.fer.hr/research/compress/algorithms/fund/lz/lzw.html

		var symbolForBitWidthIncrease = CompressorLZW.SymbolForBitWidthIncrease;
		var symbolWidthInBitsCurrent = Math.ceil
		(
			Math.log(symbolForBitWidthIncrease + 1)
			/ BitStream.NaturalLogarithmOf2
		);

		var dictionary = this.initializeDictionary(8); // hack
		var pattern = "";

		for (var i = 0; i < bytesToCompress.length; i++)
		{
			var byte = bytesToCompress[i];
			var character = String.fromCharCode(byte);
			var patternPlusCharacter = pattern + character;
			var patternPlusCharacterEscaped = "_" + patternPlusCharacter;
			if (dictionary[patternPlusCharacterEscaped] == null)
			{
				var dictionaryIndex = dictionary.length;
				dictionary[patternPlusCharacterEscaped] = dictionaryIndex;
				dictionary[dictionaryIndex] = patternPlusCharacter;

				var patternEscaped = "_" + pattern;
				var patternEncoded = dictionary[patternEscaped];

				numberOfBitsRequired = Math.ceil
				(
					Math.log(patternEncoded + 1)
					/ BitStream.NaturalLogarithmOf2
				);

				if (numberOfBitsRequired > symbolWidthInBitsCurrent)
				{
					bitStream.writeNumber
					(
						symbolForBitWidthIncrease,
						symbolWidthInBitsCurrent
					);

					symbolWidthInBitsCurrent = numberOfBitsRequired;
				}

				bitStream.writeNumber
				(
					patternEncoded,
					symbolWidthInBitsCurrent
				);

				pattern = character;
			}
			else
			{
				pattern = patternPlusCharacter;
			}

		}

		var patternEscaped = "_" + pattern;
		var patternEncoded = dictionary[patternEscaped];
		bitStream.writeNumber
		(
			patternEncoded,
			symbolWidthInBitsCurrent
		);

		bitStream.writeNumber
		(
			CompressorLZW.SymbolForBitStreamEnd,
			symbolWidthInBitsCurrent
		);

		bitStream.close();

		return bitStream.bytes;
	}

	CompressorLZW.prototype.decompressBytes = function(bytesToDecode, symbolWidthInBitsInitial)
	{
		if (symbolWidthInBitsInitial == null)
		{
			throw "Missing argument!";
		}

		var stringDecompressed = "";

		// Adapted from pseudocode found at the URL:
		// http://oldwww.rasip.fer.hr/research/compress/algorithms/fund/lz/lzw.html

		var bitStream = new BitStream(bytesToDecode);

		var dictionary = this.initializeDictionary(symbolWidthInBitsInitial);
		var symbolForClear = dictionary.length - 2;
		var symbolForBitStreamEnd = symbolForClear + 1;
		var symbolForBitWidthIncrease = null; // symbolForClear + 2;
		symbolWidthInBitsCurrent = symbolWidthInBitsInitial + 1;

		var symbolToDecode;
		var symbolDecoded;
		var pattern = "";
		var character = "";
		var patternPlusCharacter = "";

		while (bitStream.hasMoreBits() == true)
		{
			var symbolNext = bitStream.readIntegerLE
			(
				symbolWidthInBitsCurrent
			);

			pattern = dictionary[symbolToDecode];
			if (pattern == null)
			{
				pattern = "";
			}

			if (symbolNext == symbolForClear)
			{
				var dictionary = this.initializeDictionary(symbolWidthInBitsInitial);
				symbolWidthInBitsCurrent = symbolWidthInBitsInitial + 1;
			}
			else if (symbolNext == symbolForBitWidthIncrease)
			{
				symbolWidthInBitsCurrent++;
			}
			else if (symbolNext == symbolForBitStreamEnd)
			{
				break;
			}
			else
			{
				symbolToDecode = symbolNext;
				symbolDecoded = dictionary[symbolToDecode];

				if (symbolDecoded == null)
				{
					character = pattern[0];
					patternPlusCharacter = pattern + character;
					stringDecompressed += patternPlusCharacter;
				}
				else
				{
					stringDecompressed += symbolDecoded;
					character = symbolDecoded[0];
					patternPlusCharacter = pattern + character;
				}

				var patternPlusCharacterEscaped = "_" + patternPlusCharacter;
				if (dictionary[patternPlusCharacterEscaped] == null)
				{
					var dictionaryIndex = dictionary.length;

					dictionary[patternPlusCharacterEscaped] = dictionaryIndex;
					dictionary[dictionaryIndex] = patternPlusCharacter;

					if (dictionaryIndex >= Math.pow(2, symbolWidthInBitsCurrent) - 1)
					{
						symbolWidthInBitsCurrent++;
					}
				}
			}
		}

		var bytesDecompressed = [];
		for (var i = 0; i < stringDecompressed.length; i++)
		{
			bytesDecompressed.push(stringDecompressed.charCodeAt(i));
		}

		return bytesDecompressed;
	}

	CompressorLZW.prototype.initializeDictionary = function(symbolWidthInBitsInitial)
	{
		var dictionary = [];

		var numberOfSymbolsInitial = Math.pow(2, symbolWidthInBitsInitial);
		var numberOfControlSymbols = 2; // 3;

		var numberOfSymbolsTotal = numberOfSymbolsInitial + numberOfControlSymbols;

		for (var i = 0; i < numberOfSymbolsTotal; i++)
		{
			var charCode = String.fromCharCode(i);
			var charCodeEscaped = "_" + charCode;
			dictionary[charCodeEscaped] = i;
			dictionary[i] = charCode;
		}

		return dictionary;
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}

function ImageFileGIF
(
	filePath,
	gifType,
	logicalScreenSizeInPixels,
	backgroundColorIndex,
	aspectRatioType,
	globalColorTable,
	blocks
)
{
	this.filePath = filePath;
	this.gifType = gifType;
	this.logicalScreenSizeInPixels = logicalScreenSizeInPixels;
	this.backgroundColorIndex = backgroundColorIndex;
	this.aspectRatioType = aspectRatioType;
	this.globalColorTable = globalColorTable;
	this.blocks = blocks;
}
{
	// constants

	ImageFileGIF.GIFTypeStrings = [ "GIF87a", "GIF89a" ];

	// bytes

	ImageFileGIF.fromBytes = function(bytesFromFile)
	{
		var byteStream = new ByteStream(bytesFromFile);

		var gifType = byteStream.readString(6);
		if (ImageFileGIF.GIFTypeStrings.indexOf(gifType) == -1)
		{
			throw "Invalid GIF type code: " + gifType;
		}

		var logicalScreenSizeInPixels = new Coords
		(
			byteStream.readInteger(2),
			byteStream.readInteger(2)
		);

		var globalColorTableType = byteStream.readByte();

		var backgroundColorIndex = byteStream.readByte();
		var aspectRatioType = byteStream.readByte();

		var hasGlobalColorTable = ( ( (globalColorTableType >> 7) & 1 ) == 1);
		
		var globalColorTable = null;

		if (hasGlobalColorTable == true)
		{
			// Warning: 
			// The color table section of the GIF file spec at the URL
			// https://www.w3.org/Graphics/GIF/spec-gif89a.txt
			// is at best misleading and possibly erroneous.

			var colorResolution = ( (globalColorTableType >> 4) & 7) + 1;
			var areColorsSorted = ( ( (globalColorTableType >> 3) & 1) == 1);
			var colorTableSizePowerMinusOne = globalColorTableType & 7;
			var numberOfColors = Math.pow(2, colorTableSizePowerMinusOne + 1);

			globalColorTable = ImageFileGIF_ColorTable.fromBytes
			(
				byteStream,
				colorResolution, 
				areColorsSorted,
				numberOfColors
			);
		}

		var blocks = [];

		while (byteStream.hasMoreBytes() == true)
		{
			var block = ImageFileGIF.fromBytes_Block(byteStream);
			if (block != null)
			{
				blocks.push(block);
			}
		}

		var returnValue = new ImageFileGIF
		(
			"[from file]",
			gifType,
			logicalScreenSizeInPixels,
			backgroundColorIndex,
			aspectRatioType,
			globalColorTable,
			blocks
		);

		return returnValue;

	}

	ImageFileGIF.fromBytes_Block = function(byteStream)
	{
		var returnBlock = null;

		var blockType = byteStream.readByte();
		if (blockType == ImageFileGIF_Block_Image.BlockTypeCode)
		{
			returnBlock = ImageFileGIF_Block_Image.fromBytes(byteStream);
		}
		else if (blockType == ImageFileGIF_Block_Extension.BlockTypeCode)
		{
			returnBlock = ImageFileGIF_Block_Extension.fromBytes(byteStream);
		}
		else if (blockType == ImageFileGIF_Block_End.BlockTypeCode)
		{
			returnBlock = ImageFileGIF_Block_End.fromBytes(byteStream);
		}
		else
		{
			throw "Invalid block type code: " + blockType;
		}

		return returnBlock;
	}

	ImageFileGIF.prototype.toBytes = function()
	{
		var byteStream = new ByteStream([]);

		byteStream.writeString(this.gifType);

		byteStream.writeInteger(this.logicalScreenSizeInPixels.x, 2),
		byteStream.writeInteger(this.logicalScreenSizeInPixels.y, 2)

		var hasGlobalColorTable = (this.globalColorTable != null);
		var globalColorTableType = null;

		if (hasGlobalColorTable == true)
		{
			var globalColorTableBitfield = 0;
			globalColorTableBitfield |= (hasGlobalColorTable ? 1 : 0) << 7;
			globalColorTableBitfield |= (this.globalColorTable.colorResolution - 1) << 4;
			globalColorTableBitfield |= (this.globalColorTable.areColorsSorted ? 1 : 0) << 3;
			var numberOfColorsPower = Math.round
			(
				Math.log(this.globalColorTable.colors.length)
				/ Math.log(2)
			);
			globalColorTableBitfield |= (numberOfColorsPower - 1) << 0;

			globalColorTableType = globalColorTableBitfield;
		}
		else
		{
			globalColorTableType = 0; // todo
		}

		byteStream.writeByte(globalColorTableType);

		// It's frustrating that these two fields
		// don't come BEFORE the GCT stuff.
		byteStream.writeByte(this.backgroundColorIndex);
		byteStream.writeByte(this.aspectRatioType);

		if (hasGlobalColorTable == true)
		{
			this.globalColorTable.toBytes(byteStream);
		}

		for (var i = 0; i < this.blocks.length; i++)
		{
			var block = this.blocks[i];
			this.toBytes_Block(block, byteStream);
		}

		var returnValue = byteStream.bytes;

		return returnValue;
	}

	ImageFileGIF.prototype.toBytes_Block = function(block, byteStream)
	{
		var blockType = block.blockType();

		byteStream.writeByte(blockType);

		block.toBytes(byteStream);
	}

	// drawing

	ImageFileGIF.prototype.drawToGraphics = function(graphics)
	{
		var imageBlock;

		for (var i = 0; i < this.blocks.length; i++)
		{
			var block = this.blocks[i];
			if (block.blockType() == ImageFileGIF_Block_Image.BlockTypeCode)
			{
				imageBlock = block;
				break;
			}
		}

		var colorPalette = this.globalColorTable.colors;
		if (colorPalette == null)
		{
			colorPalette = imageBlock.localColorTable.colors;
		}

		var cornerNWPosInPixels = imageBlock.cornerNWPosInPixels;
		var imageSizeInPixels = imageBlock.imageSizeInPixels;
		var colorIndicesForPixels = imageBlock.pixelsAsBytes();

		for (var y = 0; y < imageSizeInPixels.y; y++)
		{
			for (var x = 0; x < imageSizeInPixels.x; x++)
			{
				var pixelIndex = y * imageSizeInPixels.x + x;
				var colorIndexForPixel = colorIndicesForPixels[pixelIndex];
				var colorComponentsRGB = colorPalette[colorIndexForPixel].componentsRGB;
				var colorAsStringRGB =
					"rgb("
					+ colorComponentsRGB[0] + ","
					+ colorComponentsRGB[1] + ","
					+ colorComponentsRGB[2]
					+ ")";
				graphics.fillStyle = colorAsStringRGB;
				graphics.fillRect
				(
					cornerNWPosInPixels.x + x,
					cornerNWPosInPixels.y + y,
					1, 1
				);
			}
		}
	}
}

function ImageFileGIF_Block_End()
{
	// todo
}
{
	ImageFileGIF_Block_End.BlockTypeCode = 0x3B; // ";"

	ImageFileGIF_Block_End.prototype.blockType = function()
	{
		return ImageFileGIF_Block_End.BlockTypeCode;
	}

	ImageFileGIF_Block_End.fromBytes = function()
	{
		return new ImageFileGIF_Block_End();
	}

	ImageFileGIF_Block_End.prototype.toBytes = function(byteStream)
	{
		// Do nothing.
	}
}

function ImageFileGIF_Block_Extension
(
	extensionType,
	numberOfDataBytes,
	isThereATransparentBackgroundColor,
	delayForAnimation,
	indexForColorTransparent,
	endOfBlockFlag
)
{
	this.extensionType = extensionType;
	this.numberOfDataBytes = numberOfDataBytes;
	this.isThereATransparentBackgroundColor = isThereATransparentBackgroundColor;
	this.delayForAnimation = delayForAnimation;
	this.indexForColorTransparent = indexForColorTransparent;
	this.endOfBlockFlag = endOfBlockFlag;
}
{
	ImageFileGIF_Block_Extension.BlockTypeCode = 0x21; // "!"

	ImageFileGIF_Block_Extension.prototype.blockType = function()
	{
		return ImageFileGIF_Block_Extension.BlockTypeCode;
	}

	// bytes

	ImageFileGIF_Block_Extension.fromBytes = function(byteStream)
	{
		var returnValue = null;

		var extensionType = byteStream.readByte();

		if (extensionType == 0xF9) // graphic control
		{
			var numberOfDataBytes = byteStream.readByte();
			var isThereATransparentBackgroundColor = (byteStream.readByte() == 1);
			var delayForAnimation = byteStream.readInteger(2);
			var indexForColorTransparent = byteStream.readByte();

			var endOfBlockFlag = byteStream.readByte();

			returnValue = new ImageFileGIF_Block_Extension
			(
				extensionType,
				numberOfDataBytes,
				isThereATransparentBackgroundColor,
				delayForAnimation,
				indexForColorTransparent,
				endOfBlockFlag
			);
		}
		else
		{
			throw "Unrecognized extension block type!";
		}

		return returnValue;
	}

	ImageFileGIF_Block_Extension.prototype.toBytes = function(byteStream)
	{
		var extensionType = this.extensionType;
		byteStream.writeByte(extensionType);

		if (extensionType == 0xF9) // graphic control
		{
			byteStream.writeByte(this.numberOfDataBytes);
			byteStream.writeByte(this.isThereATransparentBackgroundColor ? 1 : 0);
			byteStream.writeInteger(this.delayForAnimation, 2);
			byteStream.writeByte(this.indexForColorTransparent);
			byteStream.writeByte(this.endOfBlockFlag);
		}
		else
		{
			throw "Unrecognized extension block type!";
		}
	}

}

function ImageFileGIF_Block_Image
(
	cornerNWPosInPixels,
	imageSizeInPixels,
	isInterlaced,
	localColorTable,
	symbolSizeInBits,
	symbols,
	subBlocks
)
{
	this.cornerNWPosInPixels = cornerNWPosInPixels;
	this.imageSizeInPixels = imageSizeInPixels;
	this.isInterlaced = isInterlaced;
	this.localColorTable = localColorTable;
	this.symbolSizeInBits = symbolSizeInBits;
	this.symbols = symbols;
	this.subBlocks = subBlocks;
}
{
	ImageFileGIF_Block_Image.BlockTypeCode = 0x2C; // ","

	ImageFileGIF_Block_Image.prototype.blockType = function()
	{
		return ImageFileGIF_Block_Image.BlockTypeCode;
	}

	// bytes

	ImageFileGIF_Block_Image.fromBytes = function(byteStream)
	{
		var cornerNWPosInPixels = new Coords
		(
			byteStream.readInteger(2),
			byteStream.readInteger(2)
		);

		var imageSizeInPixels = new Coords
		(
			byteStream.readInteger(2),
			byteStream.readInteger(2)
		);

		var packedFields = byteStream.readByte();

		var bitStream = new BitStream([packedFields]);

		var hasLocalColorTable = ( ( (packedFields >> 7) & 1) == 1);
		var isInterlaced = ( ( (packedFields >> 6) & 1) == 1);
		var areColorsSorted = ( ( (packedFields >> 5) & 1) == 1);
		// The next 2 bits of packedFields are "reserved".

		var localColorTable;

		if (hasLocalColorTable == true)
		{
			var numberOfColorsPowerMinusOne = (packedFields & 7);
			var numberOfColors = Math.pow(2, numberOfColorsPowerMinusOne + 1);

			localColorTable = ImageFileGIF_ColorTable.fromBytes
			(
				byteStream,
				0, // todo - colorResolution,
				areColorsSorted,
				numberOfColors
			);
		}
		else
		{
			localColorTable = null;
		}

		var symbolSizeInBits = byteStream.readByte();
		var symbols = [];

		var symbolClear = Math.pow(2, symbolSizeInBits);
		var symbolEnd = symbolClear + 1;

		var subBlocks = [];

		while (true)
		{
			var numberOfBytesInSubBlock = byteStream.readByte();

			if (numberOfBytesInSubBlock == 0)
			{
				break;
			}
			else
			{
				var subBlock = byteStream.readBytes
				(
					numberOfBytesInSubBlock
				);

				subBlocks.push(subBlock);
			}
		}

		var returnBlock = new ImageFileGIF_Block_Image
		(
			cornerNWPosInPixels,
			imageSizeInPixels,
			isInterlaced,
			localColorTable,
			symbolSizeInBits,
			symbols,
			subBlocks
		);

		return returnBlock;
	}

	ImageFileGIF_Block_Image.prototype.toBytes = function(byteStream)
	{
		byteStream.writeInteger(this.cornerNWPosInPixels.x, 2);
		byteStream.writeInteger(this.cornerNWPosInPixels.y, 2);

		byteStream.writeInteger(this.imageSizeInPixels.x, 2);
		byteStream.writeInteger(this.imageSizeInPixels.y, 2);

		var hasLocalColorTable = (this.localColorTable != null);

		var packedFields = 0;
		packedFields |= (hasLocalColorTable ? 1 : 0) << 7;
		packedFields |= (this.isInterlaced ? 1 : 0) << 6;
		packedFields |= (hasLocalColorTable ? (this.localColorTable.isSorted ? 1 : 0) : 0) << 5;
		// Next two bits are "reserved".

		if (this.localColorTable == null)
		{
			byteStream.writeByte(packedFields);
		}
		else
		{
			var localColorTable = this.localColorTable;
			var numberOfColorsPower = Math.round(Math.log(localColorTable.numberOfColors) / Math.log(2));
			packedFields |= (numberOfColorsPower - 1) << 0;
			byteStream.writeByte(packedFields);

			this.toBytes_Colors
			(
				byteStream,
				this.localColorTable
			);
		}

		byteStream.writeByte(this.symbolSizeInBits);

		for (var i = 0; i < this.subBlocks.length; i++)
		{
			var subBlock = this.subBlocks[i];
			var numberOfBytesInSubBlock = subBlock.length;
			byteStream.writeByte(numberOfBytesInSubBlock);
			if (numberOfBytesInSubBlock > 0)
			{
				byteStream.writeBytes(subBlock);
			}
		}

		byteStream.writeByte(0); // No more subBlocks.
	}

	// drawing

	ImageFileGIF_Block_Image.prototype.pixelsAsBytes = function()
	{
		var subBlocksConcatenated = [];
		for (var i = 0; i < this.subBlocks.length; i++)
		{
			var subBlock = this.subBlocks[i];
			subBlocksConcatenated = subBlocksConcatenated.concat
			(
				subBlock
			);
		}

		var compressorLZW = new CompressorLZW();

		var pixelsDecompressedAsBytes = compressorLZW.decompressBytes
		(
			subBlocksConcatenated,
			this.symbolSizeInBits
		);

		return pixelsDecompressedAsBytes;
	}
}

function ImageFileGIF_ColorTable(colorResolution, areColorsSorted, colors)
{
	this.colorResolution = colorResolution; // Ignored?
	this.areColorsSorted = areColorsSorted;
	this.colors = colors;
}
{
	// bytes

	ImageFileGIF_ColorTable.fromBytes = function
	(
		byteStream, colorResolution, areColorsSorted, numberOfColors
	)
	{
		var colors = [];

		for (var i = 0; i < numberOfColors; i++)
		{
			var componentsForColor = byteStream.readBytes(3);
			var color = new Color(componentsForColor);
			colors.push(color);
		}

		var colorTable = new ImageFileGIF_ColorTable
		(
			colorResolution,
			areColorsSorted,
			colors
		);

		return colorTable;
	}

	ImageFileGIF_ColorTable.prototype.toBytes = function(byteStream)
	{
		for (var i = 0; i < this.colors.length; i++)
		{
			var color = this.colors[i];
			var colorComponents = color.componentsRGB;
			byteStream.writeBytes(colorComponents);
		}
	}
}

function Session()
{}
{
	Session.Instance = new Session();

	Session.prototype.imageFileGIF = function(value)
	{
		this.imageFileGIF = value;
		this.draw();
	}

	Session.prototype.draw = function()
	{
		var imageFileGIF = this.imageFileGIF;

		var blocks = imageFileGIF.blocks;
		for (var i = 0; i < blocks.length; i++)
		{
			var block = blocks[i];
			if (block.blockType() == ImageFileGIF_Block_Image.BlockTypeCode)
			{
				blockImage = block;
				break;
			}
		}

		var canvasSizeInPixels = blockImage.imageSizeInPixels;

		var canvas = document.createElement("canvas");
		canvas.width = canvasSizeInPixels.x;
		canvas.height = canvasSizeInPixels.y;

		var divImage = document.getElementById("divImage");
		divImage.appendChild(canvas);

		graphics = canvas.getContext("2d");

		graphics.strokeStyle = "Gray";
		graphics.strokeRect(0, 0, canvasSizeInPixels.x, canvasSizeInPixels.y);

		imageFileGIF.drawToGraphics(graphics);
	}
}

// tests

function TestFixture()
{
	// Test fixture class.
}
{
	TestFixture.prototype.bitFieldParseTest = function()
	{
		var byteBefore = 247;
		var bitStream = new BitStream([byteBefore]);
		var field0 = bitStream.readIntegerBE(1);
		var field1 = bitStream.readIntegerBE(2);
		var field2 = bitStream.readIntegerBE(3);
		var field3 = bitStream.readIntegerBE(2);

		bitStream = new BitStream([]);
		bitStream.writeIntegerBE(field0, 1);
		bitStream.writeIntegerBE(field1, 2);
		bitStream.writeIntegerBE(field2, 3);
		bitStream.writeIntegerBE(field3, 2);
		bitStream.close();
		var byteAfter = bitStream.bytes[0];

		if (byteBefore != byteAfter)
		{
			var error = "Expected: " + byteBefore + " Actual: " + byteAfter;
			throw error;
		}
		return true;
	}

	TestFixture.prototype.compressorTest = function()
	{
		var bytesToCompress = [1, 2, 3, 4, 5, 6, 7, 8];
		var compressor = new CompressorLZW();
		var bytesCompressed = compressor.compressBytes(bytesToCompress);
		var bytesDecompressed = compressor.decompressBytes(bytesCompressed, 8);
		var bytesToCompressAsString = bytesToCompress.join(",");
		var bytesDecompressedAsString = bytesDecompressed.join(",");
		if (bytesToCompressAsString != bytesDecompressedAsString)
		{
			throw "Test failed!";
		}
		return true;
	}
}

</script>
</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s