An Image Tagger in JavaScript

The JavaScript program below, when run, prompts the user to load an image file and allows the creation and editing of one or more rectangular “tags” with arbitrary size, color, and text. It also allows the user to convert the tagged image to a JSON file and back, which affords a primitive means to save and load the data.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.


<html>
<body>

<!-- ui -->

<div id="divImageTagger">

	<label><b>Image Tagger</b></label>
	<p>Load an image from a file or from JSON text.  Use the arrow keys to move the cursor, holding Shift to move faster.  Press Enter to create a new tag or select an existing one.  Press Enter multiple times to cycle through overlapping tags.  Use Shift-Enter to create a new tag within the bounds of an existing one.  Type with a tag selected to add text.  Press Escape to deselect a tag, or Delete to delete it.</p>

	<div>
		<label>Import from Image File:</label>
		<input type="file" onchange="inputImageFile_Changed(this);">
	</div>
	<div>
		<label>Tagged Image Display:</label>
		<div id="divDisplay">
			<label>[none]</label>
		</div>
	</div>
	<div>
		<label>Tag Selected:</label>
		<label>Text:</label>
		<input id="inputTagSelectedText" onchange="inputTagSelectedText_Changed(this);"></input>
		<label>Color:</label>
		<select id="selectTagSelectedColor" onchange="selectTagSelectedColor_Changed(this);">
			<option>Gray</option>
			<option>Red</option
			<option>Orange</option>
			<option>Yellow</option>
			<option>Green</option>
			<option>Blue</option>
			<option>Violet</option>
			<option>White</option>
			<option>Black</option>
		</select>
	</div>	
	<div>
		<button onclick="buttonImageWithTagsToJSON_Clicked();">v Convert Display to JSON v</button>
		<button onclick="buttonImageWithTagsFromJSON_Clicked();">^ Convert JSON to Display ^</button>	
	</div>
	<div>
		<div><label>Tagged Image as JSON:</label></div>
		<textarea id="textareaImageWithTagsAsJSON" cols="40" rows="10"></textarea>
	</div>

</div>	

<!-- ui ends -->

<script type="text/javascript">

// ui events

function inputImageFile_Changed(inputImageFile)
{
	var imageFileToLoad = inputImageFile.files[0];
	var fileReader = new FileReader();
	fileReader.onload = function(event2)
	{
		var imageLoadedAsDataURL = event2.target.result;
		var imageLoadedAsDOMElement = document.createElement("img");
		imageLoadedAsDOMElement.onload = function(event3)
		{					
			var imageLoaded = new Image(imageLoadedAsDOMElement);
			var imageWithTags = new ImageWithTags(imageLoaded, []);
			var session = Globals.Instance.session;
			session.imageWithTags = imageWithTags;
			session.initialize();
		}
		imageLoadedAsDOMElement.src = imageLoadedAsDataURL;		
	}
	fileReader.readAsDataURL(imageFileToLoad);
}

function buttonImageWithTagsFromJSON_Clicked()
{
	var textareaImageWithTagsAsJSON = document.getElementById
	(
		"textareaImageWithTagsAsJSON"
	);
	var imageWithTagsAsJSON = textareaImageWithTagsAsJSON.value;
	var imageWithTagsAsJSONObject;
	try
	{
		imageWithTagsAsJSONObject = JSON.parse(imageWithTagsAsJSON);
		var imageWithTags = ImageWithTags.fromJSONObject
		(
			imageWithTagsAsJSONObject,
			// callback
			function (imageWithTags)
			{
				var session = Globals.Instance.session;
				session.imageWithTags = imageWithTags;
				session.initialize();
			}
		);
	}
	catch (err)
	{
		alert("Invalid format!");
	}
}

function buttonImageWithTagsToJSON_Clicked()
{
	var imageWithTags = Globals.Instance.session.imageWithTags;
	if (imageWithTags == null)
	{
		alert("No image loaded!");
	}
	else
	{
		var imageWithTagsAsJSON = imageWithTags.toJSON();
		var textareaImageWithTagsAsJSON = document.getElementById
		(
			"textareaImageWithTagsAsJSON"
		);
		textareaImageWithTagsAsJSON.value = imageWithTagsAsJSON;
	}
}

function inputTagSelectedText_Changed()
{
	var session = Globals.Instance.session;
	var imageWithTags = session.imageWithTags;
	if (imageWithTags != null)
	{
		var tagSelected = session.cursor.tagSelected;
		if (tagSelected != null)
		{
			var inputTagSelectedText = document.getElementById("inputTagSelectedText");
			var text = inputTagSelectedText.value;
			tagSelected.text = text;
			session.update();
		}
	}	
}

function selectTagSelectedColor_Changed()
{
	var session = Globals.Instance.session;
	var imageWithTags = session.imageWithTags;
	if (imageWithTags != null)
	{
		var tagSelected = session.cursor.tagSelected;
		if (tagSelected != null)
		{
			var selectTagSelectedColor = document.getElementById("selectTagSelectedColor");
			var color = selectTagSelectedColor.value;
			tagSelected.color = color;
			session.update();
		}
	}	
}


// main

function main()
{
	var session = new Session();
	Globals.Instance.initialize(session);
}

// extensions 

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex >= 0)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
}
{
	Bounds.prototype.overlapWith = function(other)
	{
		var returnValue = 
		(
			(
				(
					this.min.x >= other.min.x
					&& this.min.x <= other.max.x
				)
				||
				(
					this.max.x >= other.min.x
					&& this.max.x <= other.max.x
				)
				||
				(
					other.min.x >= this.min.x
					&& other.min.x <= this.max.x
				)
				||
				(
					other.max.x >= this.min.x
					&& other.max.x <= this.max.x
				)
			)
			&&
			(
				(
					this.min.y >= other.min.y
					&& this.min.y <= other.max.y
				)
				||
				(
					this.max.y >= other.min.y
					&& this.max.y <= other.max.y
				)
				||
				(
					other.min.y >= this.min.y
					&& other.min.y <= this.max.y
				)
				||
				(
					other.max.y >= this.min.y
					&& other.max.y <= this.max.y
				)
			)
		);

		return returnValue;
	}	
}

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

	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0);
	}

	// methods

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

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

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		return this;
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	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;
	}
}

function Cursor(pos, size)
{
	this.pos = pos;
	this.size = size;

	this.sizeHalf = this.size.clone().divideScalar(2);

	this.tagSelected = null;
}
{
	// constants

	Cursor.Size = new Coords(8, 8);

	// methods

	Cursor.prototype.bounds = function()
	{
		return new Bounds(this.pos, this.pos.clone().add(this.size));
	}

	Cursor.prototype.color = function()
	{
		return (this.tagSelected == null ? "Red" : "Green");
	}

	Cursor.prototype.tagSelect = function(tagToSelect)
	{
		this.tagSelected = tagToSelect;

		if (this.tagSelected != null)
		{
			var inputTagSelectedText = document.getElementById("inputTagSelectedText");
			var selectTagSelectedColor = document.getElementById("selectTagSelectedColor");
		
			inputTagSelectedText.value = this.tagSelected.text;
			selectTagSelectedColor.value = this.tagSelected.color;
		}
	}

	// drawable

	Cursor.prototype.drawToDisplay = function(display)
	{
		display.drawRectangle
		(
			this.pos.clone().subtract(this.sizeHalf), 
			this.size,
			this.color()
		);

		if (this.tagSelected != null)
		{
			this.tagSelected.drawToDisplay(display);
			display.drawRectangle
			(
				this.tagSelected.pos.clone().subtract(this.sizeHalf), 
				this.size,
				"Green"
			)
		}
	}
}

function Display(size)
{
	this.size = size;

	this.colorBack = "White";
	this.colorFore = "Black";

	this.fontHeightInPixels = 10;
	this.font = this.fontHeightInPixels + "px sans-serif";
}
{
	Display.prototype.drawImage = function(image, pos)
	{
		this.graphics.drawImage(image.systemImage, pos.x, pos.y);
	}

	Display.prototype.drawRectangle = function(pos, size, color)
	{
		this.graphics.strokeStyle = color;
		this.graphics.strokeRect(pos.x, pos.y, size.x, size.y);
	}

	Display.prototype.drawText = function(text, pos, color)
	{
		/*
		this.graphics.strokeStyle = (color == "Black" ? "White" : "Black");
		this.graphics.strokeText
		(
			" " + text, 
			pos.x, 
			pos.y + this.fontHeightInPixels
		);
		*/

		this.graphics.fillStyle = color;
		this.graphics.fillText
		(
			" " + text, 
			pos.x, 
			pos.y + this.fontHeightInPixels
		);
	}

	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.style = "border:1px solid";
		this.canvas.width = this.size.x;
		this.canvas.height = this.size.y;

		var divDisplay = document.getElementById("divDisplay");
		divDisplay.innerHTML = "";
		divDisplay.appendChild(this.canvas);

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

		this.canvas.tabIndex = 0; // If not set, a canvas can't get focus.
		this.canvas.focus();
	}
}

function Globals()
{
	// do nothing
}
{
	// instance

	Globals.Instance = new Globals();

	// methods

	Globals.prototype.initialize = function(session)
	{
		this.session = session;
		this.session.initialize();

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();
	}
}

function Image(systemImage)
{
	this.systemImage = systemImage;

	this.size = new Coords
	(
		this.systemImage.width, this.systemImage.height
	);
}
{
	Image.prototype.drawToDisplay = function(display)
	{
		display.drawImage(this, Coords.Instances.Zeroes);
	}
}

function ImageWithTags(image, tags)
{
	this.image = image;
	this.tags = tags;
}
{
	ImageWithTags.prototype.tagsInBounds = function(boundsToCheck)
	{
		var returnValues = [];

		for (var i = 0; i < this.tags.length; i++)
		{
			var tag = this.tags[i];
			var tagBounds = tag.bounds();
			if (tagBounds.overlapWith(boundsToCheck) == true)
			{
				returnValues.push(tag);
			}
		}

		return returnValues;
	}

	// drawable

	ImageWithTags.prototype.drawToDisplay = function(display)
	{
		this.image.drawToDisplay(display);

		for (var i = 0; i < this.tags.length; i++)
		{
			var tag = this.tags[i];
			tag.drawToDisplay(display);
		}
	}

	// json

	ImageWithTags.fromJSONObject = function(imageWithTagsAsJSONObject, callback)
	{
		var imageAsDataURL = imageWithTagsAsJSONObject["imageAsDataURL"];

		var tagsAsObjects = imageWithTagsAsJSONObject["tags"];
		var tags = []; // todo
		for (var i = 0; i < tagsAsObjects.length; i++)
		{
			var tagAsObject = tagsAsObjects[i];
			var text = tagAsObject.text;
			var color = tagAsObject.color;
			var posAsObject = tagAsObject.pos;
			var pos = new Coords(posAsObject.x, posAsObject.y);
			var sizeAsObject = tagAsObject.size;
			var size = new Coords(sizeAsObject.x, sizeAsObject.y);
			var tag = new Tag(text, color, pos, size);
			tags.push(tag);
		}

		var systemImage = document.createElement("img");
		systemImage.onload = function(event)
		{
			var image = new Image(systemImage);
			var returnValue = new ImageWithTags(image, tags);
			callback(returnValue);
		}
		systemImage.src = imageAsDataURL;

	}

	ImageWithTags.prototype.toJSON = function(imageWithTagsAsJSON)
	{
		var objectToStringify = 
		{
			tags : this.tags,
			imageAsDataURL : this.image.systemImage.src
		};
		var returnValue = JSON.stringify(objectToStringify, null, 4);

		return returnValue;
	}
}


function InputHelper()
{
	this.keyPressed = null;
	this.isShiftPressed = false;

	this.keysToPreventDefaultActionFor = 
	[
		"Enter", " ",
	];
}
{
	InputHelper.prototype.clear = function()
	{
		this.keyPressed = null;
		this.isShiftPressed = null;
	}

	InputHelper.prototype.initialize = function()
	{
		var body = document.body;
		body.onkeydown = this.handleEventKeyDown.bind(this);
		body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	// events

	InputHelper.prototype.handleEventKeyDown = function(event)
	{		
		var keyPressed = event.key;

		if (this.keysToPreventDefaultActionFor.indexOf(keyPressed) >= 0)
		{
			event.preventDefault();
		}

		this.keyPressed = keyPressed;
		this.isShiftPressed = event.shiftKey;

		var elementActive = document.activeElement;
		var elementActiveTypeName = elementActive.constructor.name;
		if (elementActiveTypeName == "HTMLCanvasElement")
		{		
			Globals.Instance.session.update();
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.keyPressed = null;
	}
}

function Session(imageWithTags)
{
	this.imageWithTags = imageWithTags;
}
{
	Session.prototype.colorSelected = function()
	{
		var selectTagSelectedColor = document.getElementById("selectTagSelectedColor");
		var returnValue = selectTagSelectedColor.value;
		return returnValue;
	}

	Session.prototype.initialize = function()
	{
		if (this.imageWithTags != null)
		{
			var image = this.imageWithTags.image;
			var display = new Display(image.size);	
			display.initialize();
			Globals.Instance.display = display;
			this.cursor = new Cursor(image.size.clone().divideScalar(2), Cursor.Size);

			this.update();
		}			
	}

	Session.prototype.update = function()
	{
		if (this.cursor == null || this.imageWithTags == null)
		{
			return;
		}

		var inputHelper = Globals.Instance.inputHelper;
		var keyPressed = inputHelper.keyPressed;
		var tagSelected = this.cursor.tagSelected;
		var tagAlreadyExisted = (this.imageWithTags.tags.indexOf(tagSelected) >= 0);

		if (this.cursor == null || keyPressed == null)
		{
			// do nothing
		}
		else if (keyPressed == "Enter")
		{
			if (tagSelected == null || tagAlreadyExisted == true)
			{
				var cursorBounds = this.cursor.bounds();
				var tagsAtPos = this.imageWithTags.tagsInBounds(cursorBounds);

				if (tagsAtPos.length == 0 || inputHelper.isShiftPressed == true)
				{
					tagSelected = new Tag
					(
						"", // text
						this.colorSelected(),
						this.cursor.pos.clone(), 
						new Coords(0, 0) // size
					);
				}
				else 
				{			
					var indexOfTagSelected = tagsAtPos.indexOf(tagSelected);
					indexOfTagSelected++;
					
					if (indexOfTagSelected < tagsAtPos.length)
					{
						tagSelected = tagsAtPos[indexOfTagSelected];
					}
					else
					{
						tagSelected = null;
					}
				}
			}
			else
			{
				if (tagSelected.isValid() == true)
				{
					this.imageWithTags.tags.push(tagSelected);
				}
			}

			this.cursor.tagSelect(tagSelected);
		}
		else if (keyPressed == "Escape")
		{
			if (tagSelected != null)
			{
				this.cursor.tagSelected = null;
			}
		}
		else if (keyPressed.startsWith("Arrow") == true)
		{
			if (tagAlreadyExisted == false)
			{
				var cursorMove;

				if (keyPressed == "ArrowDown")
				{
					cursorMove = new Coords(0, 1);
				}
				else if (keyPressed == "ArrowLeft")
				{
					cursorMove = new Coords(-1, 0);
				}
				else if (keyPressed == "ArrowRight")
				{
					cursorMove = new Coords(1, 0);
				}
				else if (keyPressed == "ArrowUp")
				{
					cursorMove = new Coords(0, -1);
				}

				if (inputHelper.isShiftPressed == true)
				{
					cursorMove.multiplyScalar(8);
				}

				this.cursor.pos.add(cursorMove);

				if (tagSelected != null)
				{
					tagSelected.size.add(cursorMove);
				}
			}
		}
		else if (keyPressed == "Delete")
		{
			if (tagSelected != null)
			{
				this.imageWithTags.tags.remove(tagSelected);
				this.cursor.tagSelected = null;
			}
		}
		else if (keyPressed == "Backspace")
		{
			if (tagSelected != null)
			{
				var text = tagSelected.text;
				tagSelected.text = text.substr(0, text.length - 1);
			}
		}
		else if (keyPressed.length == 1)
		{
			if (tagSelected != null)
			{
				tagSelected.text += keyPressed;
			}
		}

		inputHelper.clear();

		this.drawToDisplay(Globals.Instance.display);
	}

	// drawable
	
	Session.prototype.drawToDisplay = function(display)
	{
		if (this.imageWithTags != null)
		{
			this.imageWithTags.drawToDisplay(display);
			this.cursor.drawToDisplay(display);
		}
	}
}

function Tag(text, color, pos, size)
{
	this.text = text;
	this.color = color;
	this.pos = pos;
	this.size = size;
}
{
	Tag.prototype.bounds = function()
	{
		return new Bounds(this.pos, this.pos.clone().add(this.size));
	}

	Tag.prototype.isValid = function()
	{
		return (this.size.magnitude() > 0);
	}

	// drawable

	Tag.prototype.drawToDisplay = function(display)
	{
		display.drawRectangle(this.pos, this.size, this.color);
		display.drawText(this.text, this.pos, this.color);
	}
}

// main

main();

</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 )

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