A JPEG Decoder in JavaScript

The JavaScript code below represents an incomplete and buggy attempt to implement a simple JPEG decoder. To see it in action, copy it into an .html file, open that file in a a web browser that runs JavaScript, and click the Upload button to load a specified JPEG image file.

At a high level, the JPEG encoding algorithm works as follows:

1. The color of each pixel in an image is translated into the YCbCr color space, in which the “Y” component represents the brightness, or “luma” of a pixel, while the “Cb” and “Cr” components encode the hue, or “chroma” of the pixel.

2. In some images, groups of neighboring chroma pixels (usually in 2 x 2 blocks) are averaged together, and only the average chroma of each group is stored. This reduces the number of chroma pixels that need to be encoded, at the expense of lowering the color resolution somewhat. This exploits the fact that human vision is much more sensitive to changes in brightness than to changes in color, so most observers won’t notice.

3. For each of the three color components (Y, Cb, and Cr), the image is split into 8 x 8 blocks of pixels.

4. Within each block, each pixel is converted from the spatial domain to the frequency domain using the discrete cosine transform, a variant of the Fourier transform. This process is described in more detail in a previous post. After this transformation, the data will still be in a 8 x 8 block of numbers, but each entry in this block will represent not a pixel color component but a particular frequency component of the block as a whole. The number in the upper-left corner of the block represents a frequency of 0. Numbers further to the right represent successively higher frequencies along the x-axis, while numbers further toward the bottom represent successively higher frequencies along the y-axis. (Yes, it’s a strange concept to grasp, and I’m not explaining it very well.)

5. Each “pixel” in the frequency-domain representation of the blocks is “quantized”. Quantization is basically just rounding. Since human vision is more sensitive to certain visual frequencies than others, however, certain frequency components of the image can be rounded to greater or lesser precision than others without an unacceptable loss of image quality. The precision to which different frequency components in an 8 x 8 block are rounded is stored in an 8 x 8 quantization table, which is in turn stored in the JPEG file’s headers. Since information is irreversably lost in this rounding, quantization is the “lossy” part of the compression algorithm.

6. The “pixels” in each block are concatenated into a sequence of bits, starting with the upper-left and proceeding in a diagonal “zigzag” order to the lower-right corner. The zigzag order is used instead of the more intuitive row-by-row order in order to enable better compression by storing neighboring, and presumably similar, pixels nearer to one another. The resulting sequence of bits is then encoded using a Huffman compression, in which the most frequent patterns of bits are replaced with the shortest “codes” of bits, with the length of the codes increasing as the frequency of the encoded pattern decreases. This results in a significant reduction in the number of bits that need to be stored. These Huffman code tables (functionally, a tree structure) are stored as part of the JPEG file’s headers.

Decoding is the opposite of encoding, except that there’s no way to reverse the rounding that was done during the quantization phase, so that step is skipped. As I say, this decoder doesn’t actually work at the moment, even with very simple images. I’m posting it now as a foundation for possible further work in the future.

<html>
<body>

<!-- user interface -->

	<input id="inputFileToLoad" type="file" onchange="inputFileToLoad_Changed(this);"></input>

<script type="text/javascript">

// user interface events

function inputFileToLoad_Changed(inputFileToLoad)
{
	var file = inputFileToLoad.files[0];

	var fileReader = new FileReader();
	fileReader.onload = inputFileToLoad_Changed_2;
	fileReader.readAsBinaryString(file);
}

function inputFileToLoad_Changed_2(event)
{
	var fileReader = event.target;
	var fileAsBinaryString = fileReader.result;
	var fileAsBitStream = BitStream.fromBinaryString(fileAsBinaryString);
	var imageJPEG = ImageJPEG.fromBitStream(fileAsBitStream);
	var imageJPEGAsDOMElement = imageJPEG.toDOMElement();
	document.body.appendChild(imageJPEGAsDOMElement);
}
// classes

function BitStream(bytes)
{
	this.bytes = bytes;
	this.byteIndexCurrent = 0;
	this.bitOffsetWithinByte = 0;
}
{
	// constants

	BitStream.BitsPerByte = 8;

	// static methods
	
	BitStream.fromBinaryString = function(binaryString)
	{
		var binaryStringAsBytes = [];

		for (var i = 0; i < binaryString.length; i++)
		{
			var charAsByte = binaryString.charCodeAt(i);
			binaryStringAsBytes.push(charAsByte);
		}

		var returnValue = new BitStream(binaryStringAsBytes);

		return returnValue;
	}

	// instance methods

	BitStream.prototype.bitsToInteger = function(bitsToConvert)
	{
		var returnValue = 0;

		var numberOfBits = bitsToConvert.length;

		for (var i = 0; i < numberOfBits; i++)
		{
			var iReversed = numberOfBits - i - 1;
			var bit = bitsToConvert[i];
			var bitValueInPlace = (bit << iReversed);
			returnValue += bitValueInPlace;
		}

		return returnValue;
	}

	BitStream.prototype.bitsToString = function(bitsToConvert)
	{
		var returnValue = "";

		var numberOfBits = bitsToConvert.length;

		for (var i = 0; i < numberOfBits; i++)
		{
			var bit = bitsToConvert[i];
			returnValue += bit;
		}

		return returnValue;
	}

	BitStream.prototype.bytesToInteger = function(bytesToConvert)
	{
		var returnValue = 0;

		var numberOfBytes = bytesToConvert.length;

		for (var i = 0; i < numberOfBytes; i++)
		{
			var iReversed = numberOfBytes - i - 1;
			var byte = bytesToConvert[i];
			var byteValueInPlace = (byte << (iReversed * BitStream.BitsPerByte));
			returnValue += byteValueInPlace;
		}

		return returnValue;
	}

	BitStream.prototype.bytesToIntegerLittleEndian = function(bytesToConvert)
	{
		var returnValue = 0;

		var numberOfBytes = bytesToConvert.length;

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = bytesToConvert[i];
			var byteValueInPlace = (byte << (i * BitStream.BitsPerByte));
			returnValue += byteValueInPlace;
		}

		return returnValue;
	}

	BitStream.prototype.hasMoreBits = function()
	{
		return (this.byteIndexCurrent < this.bytes.length);
	}

	BitStream.prototype.readBit = function()
	{
		var byteCurrent = this.bytes[this.byteIndexCurrent];
		var bitOffsetReversed = BitStream.BitsPerByte - this.bitOffsetWithinByte - 1;
		var returnValue = (byteCurrent >> bitOffsetReversed) & 1;

		this.bitOffsetWithinByte++;
		if (this.bitOffsetWithinByte >= BitStream.BitsPerByte)
		{
			this.byteIndexCurrent++;
			this.bitOffsetWithinByte = 0;
		}

		return returnValue;
	}

	BitStream.prototype.readBits = function(numberOfBitsToRead)
	{
		var returnValues = []; 

		for (var i = 0; i < numberOfBitsToRead; i++)
		{
			var bit = this.readBit();
			returnValues.push(bit);
		}

		return returnValues;
	}

	BitStream.prototype.readBitsAsInteger = function(numberOfBitsToRead)
	{
		return this.bitsToInteger(this.readBits(numberOfBitsToRead));
	}

	BitStream.prototype.readBitsAsIntegerSigned = function(numberOfBitsToRead)
	{
		var returnValue = this.readBitsAsInteger(numberOfBitsToRead);
		var max = Math.pow(2, numberOfBitsToRead) - 1;
		var halfMax = max / 2;
		if (returnValue < halfMax)
		{
			returnValue -= max;
		}

		return returnValue;
	}

	BitStream.prototype.readBitsAsString = function(numberOfBitsToRead)
	{
		return this.bitsToString(this.readBits(numberOfBitsToRead));
	}

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

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

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

		return returnValues;
	}

	BitStream.prototype.readBytesAsInteger = function(numberOfBytesToRead)
	{
		return this.bytesToInteger(this.readBytes(numberOfBytesToRead));
	}


	BitStream.prototype.readBytesAsIntegerLittleEndian = function(numberOfBytesToRead)
	{
		return this.bytesToIntegerLittleEndian(this.readBytes(numberOfBytesToRead));
	}

	BitStream.prototype.readInteger16 = function()
	{
		return this.readBytesAsInteger(2);
	}

	BitStream.prototype.readInteger24 = function()
	{
		return this.readBytesAsInteger(3);
	}

	BitStream.prototype.readInteger32 = function()
	{
		return this.readBytesAsInteger(4);
	}

	BitStream.prototype.readInteger32LittleEndian = function()
	{
		return this.readBytesAsIntegerLittleEndian(4);
	}

	BitStream.prototype.readString = function(numberOfCharactersToRead)
	{
		var returnValue = "";

		for (var i = 0; i < numberOfCharactersToRead; i++)
		{
			var charAsByte = this.readByte();
			var char = String.fromCharCode(charAsByte);
			returnValue += char;
		}

		return returnValue;
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.ceiling = function()
	{
		this.x = Math.ceil(this.x);
		this.y = Math.ceil(this.y);
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		return this;
	}

	Coords.prototype.fractional = function()
	{
		this.x -= Math.floor(this.x);
		this.y -= Math.floor(this.y);
		return this;
	}

	Coords.prototype.isInRange = function(max)
	{
		var returnValue = 
		(
			this.x >= 0
			&& this.x <= max.x
			&& this.y >= 0
			&& this.y <= max.y
		);

		return returnValue;
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}

	Coords.prototype.toString = function()
	{
		return "(" + this.x + "," + this.y + ")";
	}
}

function DiscreteCosineTransformHelper()
{
	// static class
}
{
	DiscreteCosineTransformHelper.samplesAddValue = function(samples, valueToAdd)
	{
		var returnValues = [];

		for (var i = 0; i < samples.length; i++)
		{
			var sample = samples[i];
			var sum = sample + valueToAdd;
			returnValues[i] = sum;
		}

		return returnValues;
	}

	DiscreteCosineTransformHelper.sampleArraysMultiply = function
	(
		samples0, samples1
	)
	{
		var returnValues = [];

		for (var i = 0; i < samples0.length; i++)
		{
			var sample0 = samples0[i];
			var sample1 = samples1[i];
			var productOfSamples = sample0 * sample1;
			returnValues[i] = productOfSamples;
		}

		return returnValues;
	}

	DiscreteCosineTransformHelper.samplesRound = function(samplesToRound)
	{
		var returnValues = [];

		for (var i = 0; i < samplesToRound.length; i++)
		{
			var sample = samplesToRound[i];
			returnValues[i] = Math.round(sample);
		}

		return returnValues;
	}

	DiscreteCosineTransformHelper.samples2DFrequencyToSpatialDomain = function
	(
		samplesToConvert,
		sizeInSamples
	)
	{
		var samplesConverted = [];

		for (var y = 0; y < sizeInSamples.y; y++)
		{
			for (var x = 0; x < sizeInSamples.x; x++)
			{
				var sampleConverted = 0;
				var sampleIndex = 0;

				for (var yy = 0; yy < sizeInSamples.y; yy++)
				{
					var alphaY = (yy == 0 ? 1 / Math.sqrt(2) : 1);

					for (var xx = 0; xx < sizeInSamples.x; xx++)
					{
						var alphaX = (xx == 0 ? 1 / Math.sqrt(2) : 1);
						var alphaXY = alphaX * alphaY;

						var quantity = 
							alphaXY
							* samplesToConvert[sampleIndex]
							* Math.cos
							(
								(2 * x + 1)
								* xx 
								* Math.PI
								/ 16
							)
							* Math.cos
							(
								(2 * y + 1)
								* yy 
								* Math.PI
								/ 16
							);

						sampleConverted += quantity;	
						sampleIndex++;					
					}
				}

				sampleConverted /= 4;

				samplesConverted.push(sampleConverted);
			}
		}

		return samplesConverted;
	}
}

function HuffmanTreeNode(code, encodedValue, children)
{
	this.code = code;
	this.encodedValue = encodedValue;
	this.children = children;
}
{
	HuffmanTreeNode.prototype.codeValuePairAdd = function(codeToAdd, encodedValueToAdd)
	{
		var nodeToAdd = new HuffmanTreeNode(codeToAdd, encodedValueToAdd);

		var lengthOfCodeToAdd = codeToAdd.length;

		var nodeCurrent = this;
		lengthOfNodeCurrentCode = nodeCurrent.code.length;
	
		while (lengthOfCodeToAdd > lengthOfNodeCurrentCode)
		{
			var childIndex = codeToAdd.substr(lengthOfNodeCurrentCode).charAt(0);

			if (nodeCurrent.children == null)
			{
				nodeCurrent.children = [];
			}

			var child = nodeCurrent.children[childIndex];
			if (child == null)
			{
				child = new HuffmanTreeNode
				(
					nodeCurrent.code + childIndex,
					null
				);
				nodeCurrent.children[childIndex] = child;
			}

			nodeCurrent = child;

			lengthOfNodeCurrentCode = nodeCurrent.code.length;
		}

		nodeCurrent.encodedValue = encodedValueToAdd;
	}

	HuffmanTreeNode.prototype.nodesEmptyPopulateForCodeLength = function(codeLengthMax)
	{
		if (this.code.length < codeLengthMax)
		{
			if (this.children == null)
			{
				this.children = [];
			}

			for (var i = 0; i < 2; i++)
			{
				var child = this.children[i];
				if (child == null)
				{
					child = new HuffmanTreeNode
					(
						this.code + i,
						null
					);
					this.children[i] = child;
				}
				child.nodesEmptyPopulateForCodeLength(codeLengthMax);
			}
		}
	}

	HuffmanTreeNode.prototype.nodeNextCreate = function(encodedValueToSet)
	{
		var returnValue = false;

		if (this.encodedValue == null)
		{
			if (this.children == null)
			{
				this.codeValuePairAdd
				(
					this.code + "0",
					encodedValueToSet
				);
				returnValue = true;
			}
			else
			{
				for (var i = 0; i < 2; i++)
				{
					var child = this.children[i];
					if (child == null)
					{
						this.codeValuePairAdd
						(
							this.code + i,
							encodedValueToSet
						);
						returnValue = true;
						break;
					}
					else
					{
						returnValue = child.nodeNextCreate
						(
							encodedValueToSet
						);
						if (returnValue == true)
						{
							break;
						}
					}
				}
			}
		}

		return returnValue;
	}

	HuffmanTreeNode.prototype.valueForCode = function(codeToGet)
	{
		var returnValue;

		if (this.code == codeToGet)
		{
			returnValue = this.encodedValue;
		}
		else
		{
			var childIndex = codeToGet.substr(this.code.length).charAt(0);
if (this.children == null)
{
	throw 1;
}
			var child = this.children[childIndex];
			returnValue = child.valueForCode(codeToGet);
		}

		return returnValue;
	}
}

function ImageJPEG(name, frames)
{
	this.name = name;
	this.frames = frames;
}
{
	// static methods

	ImageJPEG.fromBitStream = function(stream)
	{
		// todo

		var sizeInPixels = new Coords(8, 8); // todo

		var pixels = [];
		
		for (var y = 0; y < sizeInPixels.y; y++)
		for (var x = 0; x < sizeInPixels.x; x++)
		{
			var pixelIndex = y * sizeInPixels.x + x;
			var pixel = 0; // todo
			pixels[pixelIndex] = pixel;
		}

		var frames = [];
		var huffmanTrees = [];
		var quantizationTables = [];

		var frameCurrent = null;

		var jpegIdentifier = stream.readBytesAsInteger(2);

		if (jpegIdentifier != 0xFFD8)
		{
			throw "Not a JPEG file.";
		}

		while (stream.hasMoreBits() == true)
		{
			var segmentMarker = stream.readByte();
			if (segmentMarker != 0xFF)
			{
				throw "Expected a segment marker (0xFF), but read: " + segmentMarker;
			}

			var segmentType = stream.readByte();

			if (segmentType == 0xC0)
			{
				// start of frame - baseline DCT
				var payloadLengthInBytes = stream.readBytesAsInteger(2) - 2;
				var payloadAsBytes = stream.readBytes(payloadLengthInBytes);
				var frameHeaderAsStream = new BitStream(payloadAsBytes);
				ImageJPEG.fromBitStream_Segment_StartOfFrame
				(
					frameHeaderAsStream, frames
				);
				frameCurrent = frames[frames.length - 1];
			}
			else if (segmentType == 0xC2)
			{
				// start of frame - progressive DCT
				var payloadLengthInBytes = stream.readBytesAsInteger(2) - 2;
				var payloadAsBytes = stream.readBytes(payloadLengthInBytes);
			}
			else if (segmentType == 0xC4)
			{
				// define Huffman tables
				var payloadLengthInBytes = stream.readBytesAsInteger(2) - 2;
				var payloadAsBytes = stream.readBytes(payloadLengthInBytes);
				var payloadAsStream = new BitStream(payloadAsBytes);
				ImageJPEG.fromBitStream_Segment_HuffmanTree
				(
					payloadAsStream, huffmanTrees
				);
			}
			else if (segmentType >= 0xD0 && segmentType <= 0xD7)
			{
				// restart
				// no payload
			}
			else if (segmentType == 0xD8)
			{
				// start of image
				// no payload
			}
			else if (segmentType == 0xD9)
			{
				// end of image
				// no payload
				break;
			}
			else if (segmentType == 0xDA)
			{
				// start of scan
				var payloadLengthInBytes = stream.readBytesAsInteger(2) - 2;
				var payloadAsBytes = stream.readBytes(payloadLengthInBytes);

				var payloadAsStream = new BitStream(payloadAsBytes);
				// Not sure what these do...
				var mysteryBytes = payloadAsStream.readBytes(payloadLengthInBytes);

				var returnValues1 = ImageJPEG.fromBitStream_Segment_Scan
				(
					stream,
					huffmanTrees,
					quantizationTables,
					frameCurrent
				);
			}
			else if (segmentType == 0xDB)
			{
				// define quantization tables
				var payloadLengthInBytes = stream.readBytesAsInteger(2) - 2;
				var payloadAsBytes = stream.readBytes(payloadLengthInBytes);
				var payloadAsStream = new BitStream(payloadAsBytes);
				ImageJPEG.fromBitStream_Segment_QuantizationTable
				(
					payloadAsStream, quantizationTables
				);
			}
			else if (segmentType == 0xDD)
			{
				// define restart interval
				var payloadLengthInBytes = stream.readBytesAsInteger(2) - 2;
				if (payloadLengthInBytes != 4)
				{
					throw "Invalid length for define restart interval segment."
				}
				var payloadAsBytes = stream.readBytes(payloadLengthInBytes);
			}
			else if (segmentType >= 0xE0 && segmentType <= 0xEF)
			{
				// application-specific
				var payloadLengthInBytes = stream.readBytesAsInteger(2) - 2;
				var payloadAsBytes = stream.readBytes(payloadLengthInBytes);
			}
			else if (segmentType == 0xFE)
			{
				// text comment
				var payloadLengthInBytes = stream.readBytesAsInteger(2) - 2;
				var payloadAsBytes = stream.readBytes(payloadLengthInBytes);
			}
			else
			{
				throw "Unrecognized segment type: " + segmentType;
			}
		}

		var returnValue = new ImageJPEG
		(
			"todo",
			frames
		);		

		return returnValue;
	}

	ImageJPEG.fromBitStream_Segment_HuffmanTree = function(payloadAsStream, huffmanTrees)
	{
		while (payloadAsStream.hasMoreBits() == true)
		{
			var tableIndex = payloadAsStream.readByte();

			var huffmanTree = new HuffmanTreeNode("", null);
			var numbersOfHuffmanEntriesForCodeBitLengths = payloadAsStream.readBytes(16);

			for (var i = 0; i < numbersOfHuffmanEntriesForCodeBitLengths.length; i++)
			{
				var numberOfEntriesForCodeBitLength = numbersOfHuffmanEntriesForCodeBitLengths[i];

				for (var j = 0; j < numberOfEntriesForCodeBitLength; j++)
				{
					var encodedValue = payloadAsStream.readByte();
					huffmanTree.nodeNextCreate(encodedValue);
				}

				huffmanTree.nodesEmptyPopulateForCodeLength(i + 1);
			}

			//huffmanTrees[tableIndex] = huffmanTree;
			// hack - Not sure what to do here.
			huffmanTrees.push(huffmanTree);
		}
	}

	ImageJPEG.fromBitStream_Segment_QuantizationTable = function
	(
		payloadAsStream, quantizationTables
	)
	{
		while (payloadAsStream.hasMoreBits() == true)
		{
			var header = payloadAsStream.readByte();
			var quantizationIndex = (header & 0xF);
			var quantizationTable = [];
			for (var i = 0; i < 64; i++)
			{
				var quant = payloadAsStream.readByte();
				quantizationTable[i] = quant;
			}
			quantizationTables.push(quantizationTable);
		}
	}

	ImageJPEG.fromBitStream_Segment_Scan = function
	(
		stream,
		huffmanTrees,
		quantizationTables,
		frame
	)
	{
		var scanBytes = [];
		while (true)
		{
			var scanByte = stream.readByte();
			if (scanByte == 0xFF)
			{
				var scanByteNext = stream.readByte();
				if (scanByteNext == 0)
				{
					// Ignore the 0, as it simply indicates
					// the preceding 0xFF is scan data, 
					// rather than a new segment marker.
				}
				else
				{
					// Next segment: back up the stream.
					stream.byteIndexCurrent -= 2;
					break;
				}
			}

			scanBytes.push(scanByte); 					
		}

		var scanBytesAsBitStream = new BitStream(scanBytes);

		var huffmanCodeInProgress = "";
		var decodedValues = [];
		var huffmanTreeIndex = 0;
		var huffmanTree = huffmanTrees[huffmanTreeIndex];

		// There are 3 planes:
		// luma, chromaBlue, and chromaRed.

		var blocks = []; 
		var planes = [];
		var planeDCAndACs = [];
				
		while (scanBytesAsBitStream.hasMoreBits() == true)
		{
			var bit = scanBytesAsBitStream.readBit();
			huffmanCodeInProgress += bit;
			var encodedValue = huffmanTree.valueForCode
			(
				huffmanCodeInProgress
			);
			if (encodedValue != null)
			{
				if (planeDCAndACs.length == 0) 
				{		
					var planeDC;

					if (encodedValue == 0)
					{
						planeDC = 0;
					}
					else
					{
						var bitLength = encodedValue;
						planeDC = scanBytesAsBitStream.readBitsAsIntegerSigned
						(
							bitLength
						);
					}

					planeDCAndACs.push(planeDC);
					huffmanTreeIndex += 2;
					huffmanTree = huffmanTrees[huffmanTreeIndex];
				}
				else 
				{
					// ACs
					if (encodedValue == 0)
					{
						// end of block
						// Any remaining AC components are 0.

						var numberOfCoefficientsRemaining = 64 - planeDCAndACs.length;
						for (var a = 0; a < numberOfCoefficientsRemaining; a++)
						{
							planeDCAndACs.push(0);
						}

						planes.push(planeDCAndACs);
						planeDCAndACs = [];

						if (planes.length == 3)
						{
							blocks.push(planes);
							planes = [];
							huffmanTreeIndex = 0;
						}
						else
						{
							huffmanTreeIndex = 1;
						}

						huffmanTree = huffmanTrees[huffmanTreeIndex];
					}
					else
					{
						planeDCAndACs.push(encodedValue);
					}
				}

				huffmanCodeInProgress = "";
			}
		}
				
		var blockPrev = [ [0], [0], [0] ];
		var cells = [];
		var cellOffsets = 
		[
			new Coords(1, -1),
			new Coords(1, 0),
			new Coords(-1, 1),
			new Coords(0, 1),
		];
		var cellOffsetIndex = 0;
		var cellOffset = cellOffsets[cellOffsetIndex];
		var blockSizeInCells = new Coords(8, 8);
		var blockSizeInCellsMinusOnes = blockSizeInCells.clone().subtract
		(
			new Coords(1, 1)
		);
		var cellsPerBlock = blockSizeInCells.x * blockSizeInCells.y;
		var frameSizeInPixels = frame.header.sizeInPixels;
		var frameSizeInBlocks = frameSizeInPixels.clone().divide(blockSizeInCells);
		var pixels = [];

		var matrixHelper = DiscreteCosineTransformHelper;

		for (var b = 0; b < blocks.length; b++)
		{
			var block = blocks[b];
			var blockPlanes = block;

			var blockPos = new Coords
			(
				Math.floor(b / frameSizeInBlocks.x),
				b % frameSizeInBlocks.x
			);

			var blockPosInCells = blockPos.clone().multiply(blockSizeInCells);
			var cellPos = new Coords(0, 0);
			var cellPosNext = new Coords(0, 0);

			for (var p = 0; p < blockPlanes.length; p++)
			{
				var plane = blockPlanes[p];
				var planeFromBlockPrev = blockPrev[p];

				var dcDelta = plane[0];
				var dcPrev = planeFromBlockPrev[0];
				var dcAbsolute = dcPrev + dcDelta;
				plane[0] = dcAbsolute;

				var quantizationTableIndex = (p == 0 ? 0 : 1);
				var quantizationTable = quantizationTables[quantizationTableIndex];

				var planeDequantized = matrixHelper.sampleArraysMultiply
				(
					plane, 
					quantizationTable
				);

				var planeInSpatialDomain = matrixHelper.samples2DFrequencyToSpatialDomain
				(
					planeDequantized,
					blockSizeInCells
				);

				planeInSpatialDomain = matrixHelper.samplesAddValue
				(
					planeInSpatialDomain, 128 // hack
				); 

				planeInSpatialDomain = matrixHelper.samplesRound
				(
					planeInSpatialDomain
				); 
	
				for (var i = 0; i < cellsPerBlock; i++)
				{
					cellPosNext.overwriteWith
					(
						cellPos
					).add
					(
						cellOffset
					);

					if (cellPosNext.isInRange(blockSizeInCells) == false)
					{
						cellOffsetIndex++;
						cellOffset = cellOffsets[cellOffsetIndex];
						cellPosNext.overwriteWith
						(
							cellPos
						).add
						(
							cellOffset
						);	
						cellOffsetIndex++;
						cellOffsetIndex %= cellOffsets.length;
						cellOffset = cellOffsets[cellOffsetIndex];
					}

					cellPos.overwriteWith(cellPosNext);
				
					cellPos.add
					(
						blockPosInCells
					);

					if (p == 0) // hack - luma only for now
					{
if (b == 1)
{
	var one = 1;
}
						var pixelLuma = Math.floor(planeInSpatialDomain[i] * 100 / 255);
						var pixelIndex = cellPos.y * frameSizeInPixels.x + cellPos.x;
						var pixelColor = "hsl(0,0%," + pixelLuma + "%)";
						pixels[pixelIndex] = pixelColor;
					}
				}
			}

			blockPrev = block;
		}

		frame.pixels = pixels;
	}

	ImageJPEG.fromBitStream_Segment_StartOfFrame = function(frameHeaderAsStream, frames)
	{
		var samplePrecision = frameHeaderAsStream.readByte();

		// X and Y seem to be reversed--why?
		var sizeInPixels = new Coords
		(
			frameHeaderAsStream.readInteger16(),
			frameHeaderAsStream.readInteger16()
		);

		var numberOfImageComponents = frameHeaderAsStream.readByte();
		var componentIdentifier = frameHeaderAsStream.readByte();
		var samplingFactors = new Coords
		(
			frameHeaderAsStream.readBits(4),
			frameHeaderAsStream.readBits(4)
		);
		var quantizationTableDestinationSelector = frameHeaderAsStream.readByte();
				
		var frameHeader = new ImageJPEG_Frame_Header
		(
			samplePrecision,
			sizeInPixels,
			numberOfImageComponents,
			componentIdentifier,
			samplingFactors,
			quantizationTableDestinationSelector
		);

		var frame = new ImageJPEG_Frame(frameHeader);

		frames.push(frame);
	}

	// instance methods

	ImageJPEG.prototype.toDOMElement = function()
	{
		var frame0Header = this.frames[0].header;

		var canvas = document.createElement("canvas");
		canvas.width = frame0Header.sizeInPixels.x;
		canvas.height = frame0Header.sizeInPixels.y;
		canvas.style.border = "1px solid";

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

		var frame0 = this.frames[0];
		var frameSizeInPixels = frame0.header.sizeInPixels;
		var frame0Pixels = frame0.pixels;

		for (var y = 0; y < frameSizeInPixels.y; y++)
		{
			for (var x = 0; x < frameSizeInPixels.x; x++)
			{
				var pixelIndex = y * frameSizeInPixels.x + x;
				var pixelColor = frame0Pixels[pixelIndex];
				graphics.fillStyle = pixelColor;
				graphics.fillRect(x, y, 1, 1);
			}
		}

		return canvas;
	}
}

function ImageJPEG_Frame(header, pixels)
{
	this.header = header;
	this.pixels = pixels;
}

function ImageJPEG_Frame_Header
(
	samplePrecision,
	sizeInPixels,
	numberOfImageComponents,
	componentIdentifier,
	samplingFactors,
	quantizationTableDestinationSelector
)
{
	this.samplePrecision = samplePrecision;
	this.sizeInPixels = sizeInPixels;
	this.numberOfImageComponents = numberOfImageComponents;
	this.componentIdentifier = componentIdentifier;
	this.samplingFactors = samplingFactors;
	this.quantizationTableDestinationSelector = quantizationTableDestinationSelector;
}

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

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

One Response to A JPEG Decoder in JavaScript

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s