Getting Started with Knockout.JS

The instructions below can be followed to create a simple task list application in Knockout.js. Knockout.js is an open-source JavaScript web framework that implements the “MVVM” (“Model-View-ViewModel”) software pattern. It was developed by Steve Sanderson while employed at Microsoft.

These instructions are a simplified version of a tutorial hosted the URL “http://learn.knockoutjs.com”.

KnockoutJS-TaskList.png

1. In any convenient location, create a new directory named “KnockoutJS”.

2. Download the current version of Knockout.js, place it in the newly created KnockoutJS directory, and rename the file to “knockout.js”. As of this writing, the latest version is available for download at the URL “http://knockoutjs.com/downloads/index.html”.

3. Still in the KnockoutJS directory, create a new text file named “TaskList-View.html”, containing the following text.


<!-- This code is minimally adapted from a sample at the URL "http://learn.knockoutjs.com". -->

<html>
<body>

<script type="text/javascript" src="knockout.js"></script>

<h3>Tasks</h3>

<form data-bind="submit: addTask">
    Add task: <input data-bind="value: newTaskText" placeholder="What needs to be done?" />
    <button type="submit">Add</button>
</form>

<ul data-bind="foreach: tasks, visible: tasks().length > 0">
    <li>
        <input type="checkbox" data-bind="checked: isDone" />
        <input data-bind="value: title, disable: isDone" />
        <a href="#" data-bind="click: $parent.removeTask">Delete</a>
    </li> 
</ul>

You have <b data-bind="text: incompleteTasks().length"> </b> incomplete task(s)
<span data-bind="visible: incompleteTasks().length == 0"> - it's beer time!</span>

<script type="text/javascript" src="TaskList-ViewModel.js"></script>

</body>
</html>

4. Still in the KnockoutJS directory, create a new text file named “TaskList-ViewModel.js”, containing the following text.


// This code is minimally adapted from a sample at the URL 
// http://learn.knockoutjs.com 

function Task(data) {
	this.title = ko.observable(data.title);
	this.isDone = ko.observable(data.isDone);
}

function TaskListViewModel() {
	// Data
	var self = this;
	self.tasks = ko.observableArray([]);
	self.newTaskText = ko.observable();
	self.incompleteTasks = ko.computed(function() {
		return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.isDone() });
	});

	// Operations
	self.addTask = function() {
		self.tasks.push(new Task({ title: this.newTaskText() }));
		self.newTaskText("");
	};
	self.removeTask = function(task) { self.tasks.remove(task) };
}

ko.applyBindings(new TaskListViewModel());

4. Open the file “TaskList.html” in a web browser that runs JavaScript. Verify that a task list interface appears, and that it works as expected.

Posted in Uncategorized | Tagged , , , , , , | Leave a comment

Getting Started with ReactJS

The instructions below can be followed to create a simple user interface in React.js. React.js is an open-source user interface library for JavaScript originated by Facebook.

These instructions are a simplified version of the tutorial found on React.js’s official website, currently available at the URL
“https://facebook.github.io/react/tutorial/tutorial.html”
.

TicTacToeViaReactJS.png

1. If you have not already done so, install Node.js. As of this writing, the latest version is available for download at the URL “https://nodejs.org/en/download/”.

2. In any convenient location, create a new directory named “ReactJS”.

3. Open a command prompt, navigate to the ReactJS directory created in the previous step, and run the following command to install the “create-react-app” utility:

	npm install -g create-react-app

4. Still in the command prompt, run the following commands and wait for them to complete. A new React.js application called “my-app” will be created within the ReactJS directory.

	create-react-app my-app
	cd my-app
	npm start

5. Navigate to the “src” directory within the newly created “my-app” directory.

6. Delete the existing contents of the src directory.

7. Still within the src directory, create a new text file named “index.css”, containing the following text.


/* 
 * This code is minimally adapted from a sample found at the URL
 * https://facebook.github.io/react/tutorial/tutorial.html 
 */

body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

8. Still within the src directory, create a new text file named “index.js”, containing the following text.


// This code is minimally adapted from a sample found at the URL
// https://facebook.github.io/react/tutorial/tutorial.html

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      history: [
        {
          squares: Array(9).fill(null)
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ? "Move #" + move : "Game start";
      return (
        <li key={move}>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(<Game />, document.getElementById("root"));

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

10. Back in the command prompt, navigate to the “my-app” directory and run the command “npm start” to start the application.

11. If it is not done automatically, open a web browser and navigate to the URL “http://localhost:3000”. Verify that a tic-tac-toe grid is displayed.

12. Click the “Game Start” link, then click on the various cells of the tic-tac-toe to play the game.

Posted in Uncategorized | Tagged , , , , , , | Leave a comment

A Flood-Fill Implementation in JavaScript

Below is a simple implementation of a floodfill algorithm in JavaScript. When run, the program will draw a green background on a canvas, render the word “FLOOD” over it in orange, and then flood-fill the background with cyan, leaving the interiors (and outside edges, due to a “tolerance” setting of 0) of the “O” and “D” characters with the original background. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

The floodFill() function extends the existing CanvasRenderingContext2D class.
The function takes as its arguments the x and y position of the first pixel to fill, followed by an argument named “colorDifferenceTolerance”. The algorithm uses the A* (“A-star”) algorithm to identify contiguous pixels to be filled with the specified fill color. If the color tolerance is set to 0, as in the sample below, only contiguous pixels that exactly match the color of the first pixel will be overwritten with the fill color.

Floodfill.png


<html>
<body>


<script type="text/javascript">

// main
function main()
{
    var canvasSize = new Coords(100, 100);

    var canvas = document.createElement("canvas");
    canvas.width = canvasSize.x;
    canvas.height = canvasSize.y;
    document.body.appendChild(canvas);

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

    graphics.fillStyle = "DarkGreen";
    graphics.fillRect(0, 0, canvasSize.x, canvasSize.y);

    var fontHeight = canvasSize.y * .25;
    graphics.font = fontHeight + "px sans-serif";
    graphics.fillStyle = "Orange";
    graphics.fillText
    (
        "FLOOD", 
        fontHeight * .25, 
        (canvasSize.y - fontHeight) / 2 + fontHeight 
    );

    graphics.fillStyle = "Cyan";
    graphics.floodFill(0, 0, 0);
}

// extensions

function CanvasRenderingContext2DExtensions()
{
    // extension class
}
{
    CanvasRenderingContext2D.prototype.floodFill = function(x, y, colorDifferenceTolerance)
    {    
        var canvas = this.canvas;
        var imageSize = new Coords(canvas.width, canvas.height);
        var imageSizeMinusOnes = imageSize.clone().subtract(new Coords(1, 1));

        var colorToFillOverRGBA = this.getImageData(x, y, 1, 1).data;

        var pixelPos = new Coords(x, y);
        var pixelIndexStart = pixelPos.y * imageSize.x + pixelPos.x;
        var pixelIndicesToTest = [ pixelIndexStart ];
        var pixelIndicesAlreadyTested = [];

        var neighborOffsets = 
        [
            new Coords(-1, 0),
            new Coords(1, 0),
            new Coords(0, -1),
            new Coords(0, 1)
        ];

        while (pixelIndicesToTest.length > 0)
        {
            var pixelIndex = pixelIndicesToTest[0];
            pixelIndicesToTest.splice(0, 1);
            pixelIndicesAlreadyTested[pixelIndex] = pixelIndex;

            pixelPos.x = pixelIndex % imageSize.x;
            pixelPos.y = Math.floor(pixelIndex / imageSize.x);

            var pixelRGBA = this.getImageData(pixelPos.x, pixelPos.y, 1, 1).data;
            var pixelDifference = Math.abs
            (
                pixelRGBA[0] - colorToFillOverRGBA[0]
                + pixelRGBA[1] - colorToFillOverRGBA[1]
                + pixelRGBA[2] - colorToFillOverRGBA[2]
            );

            if (pixelDifference <= colorDifferenceTolerance)
            {
                this.fillRect(pixelPos.x, pixelPos.y, 1, 1);

                var neighborPos = new Coords();

                for (var n = 0; n < neighborOffsets.length; n++)
                {
                    var neighborOffset = neighborOffsets[n];

                    neighborPos.overwriteWith
                    (
                        pixelPos
                    ).add
                    (
                        neighborOffset
                    );

                    if (neighborPos.isInRange(imageSize) == true)
                    {
                        var neighborIndex = 
                            neighborPos.y * imageSize.x + neighborPos.x;
                        var isPixelIndexAlreadyUnderConsideration = 
                        (
                            pixelIndicesToTest.indexOf(neighborIndex) >= 0 
                            || pixelIndicesAlreadyTested[neighborIndex] != null
                        )  
                        if (isPixelIndexAlreadyUnderConsideration == false)
                        {
                            pixelIndicesToTest.push(neighborIndex);
                        }
                    }
                }
            }                
        }
    }
}

// classes

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.clone = function()
    {
        return new Coords(this.x, this.y);
    }

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

// run

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

A Visual Acuity Chart Generator in JavaScript

The JavaScript code below, when run, will generate a random visual acuity chart. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This code is just a prototype. The math on the chart may not be quite be correct at the moment. If in doubt, it might be better to consult a licensed optometrist.


<html>
<body>

<!-- ui -->

<div>
	<label>Pixels per Inch:</label>
	<input id="inputPixelsPerInch" type="number" value="150"></input>
</div>

<div>
	<label>Chart Size in Inches:</label>
	<input id="inputChartSizeInInchesX" type="number" value="8.5"></input>
	<label>x</label>
	<input id="inputChartSizeInInchesY" type="number" value="11"></input>
</div>

<div>
	<label>Size of Top Line in Inches:</label>
	<input id="inputTopLineSizeInInches" type="number" value="1"></input>
</div>

<div>
	<label>Number of Lines:</label>
	<input id="inputNumberOfLines" type="number" value="8"></input>
</div>

<button onclick="buttonGenerate_Clicked();">Generate</button>

<div id="divOutput"></div>

<!-- ui ends -->

<script type="text/javascript">

// ui event handlers

function buttonGenerate_Clicked()
{
	var inputPixelsPerInch = 
		document.getElementById("inputPixelsPerInch");
	var pixelsPerInchAsString = inputPixelsPerInch.value;
	var pixelsPerInch = parseInt(pixelsPerInchAsString);

	var inputChartSizeInInchesX = 
		document.getElementById("inputChartSizeInInchesX");
	var inputChartSizeInInchesY = 
		document.getElementById("inputChartSizeInInchesY");

	var chartSizeInInches = new Coords
	(
		parseFloat(inputChartSizeInInchesX.value),
		parseFloat(inputChartSizeInInchesY.value)
	);
	
	var inputTopLineSizeInInches = 
		document.getElementById("inputTopLineSizeInInches");
	var topLineSizeInInchesAsString = 
		inputTopLineSizeInInches.value;
	var topLineSizeInInches = 
		parseFloat(topLineSizeInInchesAsString);

	var inputNumberOfLines = 
		document.getElementById("inputNumberOfLines");
	var numberOfLinesAsString = 
		inputNumberOfLines.value;
	var numberOfLines = 
		parseInt(numberOfLinesAsString);

	var visionChart = new VisionChart
	(
		pixelsPerInch, 
		chartSizeInInches,
		topLineSizeInInches,
		numberOfLines
	);

	var visionChartAsCanvas = visionChart.toCanvas();
	
	var divOutput = document.getElementById("divOutput");
	divOutput.innerHTML = "";
	divOutput.appendChild(visionChartAsCanvas);
}

// classes

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.clone = function()
	{
		return new Coords(this.x, this.y);
	}

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

	Coords.prototype.right = function()
	{
		var temp = this.y;
		this.y = this.x;
		this.x = 0 - temp;
		return this;
	}

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

}

function VisionChart
(
	pixelsPerInch, 
	chartSizeInInches, 
	topLineSizeInInches, 
	numberOfLines
)
{
	this.pixelsPerInch = pixelsPerInch;
	this.chartSizeInInches = chartSizeInInches;
	this.topLineSizeInInches = topLineSizeInInches;

	this.chartSizeInPixels = 
		this.chartSizeInInches.clone().multiplyScalar
		(
			pixelsPerInch
		);

	this.topLineSizeInPixels = 
		this.topLineSizeInInches * pixelsPerInch;

	var radiansPerTurn = Math.PI * 2;
	var degreesPerTurn = 360;
	var minutesPerDegree = 60;
	var angleSubtendedBySmallestSymbolInMinutes = 5;
	var angleSubtendedBySmallestSymbolInRadians = 
		radiansPerTurn 
		* angleSubtendedBySmallestSymbolInMinutes 
		/ (degreesPerTurn * minutesPerDegree);

	this.distanceFromEyeInInches = 
		this.topLineSizeInInches 
		/ numberOfLines
		/ Math.sin
		(
			angleSubtendedBySmallestSymbolInRadians 			
		);

	this.numberOfLines = numberOfLines;
}
{
	VisionChart.prototype.toCanvas = function()
	{
		var returnValue = document.createElement("canvas");
		returnValue.style = "border:1px solid";
		returnValue.width = this.chartSizeInPixels.x;
		returnValue.height = this.chartSizeInPixels.y;

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

		graphics.fillStyle = "White";
		graphics.fillRect
		(
			0, 0, 
			this.chartSizeInPixels.x, 
			this.chartSizeInPixels.y
		);

		graphics.fillStyle = "Black";		

		var inchesPerFoot = 12;
		var message = 
			"This chart should be placed " 
			+ Math.floor(this.distanceFromEyeInInches / inchesPerFoot)
			+ " feet + "
			+ Math.floor(this.distanceFromEyeInInches % inchesPerFoot)
			+ " inches from the eye.";

		var fontHeightInPixels = 10;
		graphics.font = fontHeightInPixels + "px sans-serif";
		graphics.fillText(message, 0, fontHeightInPixels);

		var drawPos = new Coords(0, this.topLineSizeInPixels);

		var directions = 
		[
			new Coords(1, 0),
			new Coords(0, 1),
			new Coords(-1, 0),
			new Coords(0, -1),
		];

		var symbolSizeInPixelsOfBottomLine = 
			this.topLineSizeInPixels / this.numberOfLines

		for (var i = 0; i < this.numberOfLines; i++)
		{
			var numberOfSymbolsOnLine = i + 1;

			var symbolSizeInPixels = 
				this.topLineSizeInPixels 
				/ numberOfSymbolsOnLine;

			var widthOfLineInPixels = 
				symbolSizeInPixels 
				* (numberOfSymbolsOnLine * 2 - 2);

			drawPos.x = 
				(this.chartSizeInPixels.x - widthOfLineInPixels) 
				/ 2;

			for (var s = 0; s < numberOfSymbolsOnLine; s++)
			{
				var directionIndex = Math.floor
				(
					Math.random() * directions.length
				);

				var direction = directions[directionIndex];

				this.drawSymbol(graphics, symbolSizeInPixels, drawPos, direction);

				drawPos.x += symbolSizeInPixels * 2;	
			}

			var acuityQuotient = 
				symbolSizeInPixels / symbolSizeInPixelsOfBottomLine;
			var acuityNumerator = 20;
			var acuityDenominator = Math.round(acuityQuotient * acuityNumerator);

			graphics.fillText
			(
				acuityNumerator + "/" + acuityDenominator, 
				this.chartSizeInPixels.x - this.topLineSizeInPixels,
				drawPos.y
			)

			drawPos.y += 
				Math.sqrt(symbolSizeInPixels) 
				* this.topLineSizeInPixels 
				/ this.numberOfLines; // hack
		}		
	
		return returnValue;
	}

	VisionChart.prototype.drawSymbol = function(graphics, symbolSizeInPixels, drawPosOriginal, direction)
	{
		var rightOne = direction.clone().multiplyScalar(symbolSizeInPixels);
		var rightOneHalf = rightOne.clone().multiplyScalar(.5);
		var rightFourFifths = rightOne.clone().multiplyScalar(.8);
		var rightThreeFifths = rightOne.clone().multiplyScalar(.6);
		var downOne = rightOne.clone().right();
		var downOneHalf = downOne.clone().multiplyScalar(.5);
		var downOneFifth = downOne.clone().multiplyScalar(.2);

		var drawPos = drawPosOriginal.clone().subtract
		(
			rightOneHalf
		).subtract
		(
			downOneHalf
		);

		graphics.beginPath();
	
		// draw an E-shaped symbol

		// top limb 
		graphics.moveTo(drawPos.x, drawPos.y);
		drawPos.add(rightOne);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.subtract(rightFourFifths);
		graphics.lineTo(drawPos.x, drawPos.y);

		// middle limb
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(rightThreeFifths);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.subtract(rightThreeFifths);
		graphics.lineTo(drawPos.x, drawPos.y);

		// bottom limb
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(rightFourFifths);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.subtract(rightOne);
		graphics.lineTo(drawPos.x, drawPos.y);

		graphics.closePath();
		graphics.fill();
	}
}


</script>

</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

Isolating Characters in an Image in JavaScript

The JavaScript program below first draws horizontal separators between lines of text in an imported image, and then draws separators between individual characters in each line. Along with a few other recent posts, it is intended as the beginnings of a system for optical character recognition. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.


<html>
<body>

<!-- ui -->

<div id="divUI">

	<label><b>Image Characterizer</b></label>
	<p>Load a file, specify a background color, and click the button to automatically split into lines and characters.</p>
	
	<div>
		<label>Image to Find Characters in:</label>
		<input type="file" onchange="inputImageToCharacterize_Changed(this);"></input>
		<div id="divDisplayImageToCharacterize">[none]</div>
	</div>

	<div>
		</div>
			<label>Background Color RGB:</label>
			<input id="inputColorBackground" value="255,255,255"></input>
		</div>
		<div>
			<label>Color Difference Threshold:</label>
			<input id="inputColorDifferenceThreshold" type="number" value="256"></input>
		</div>
		<button onclick="buttonImageCharacterize_Clicked();">Find Characters</button>
	</div>

	<div>
		<label>Image with Characters Highlighted:</label>
		<div id="divDisplayImageCharacterized">[none]</div>
	</div>

</div>

<!-- ui ends-->

<script type="text/javascript">

// ui events

function buttonImageCharacterize_Clicked()
{
	var divDisplayImageToCharacterize = 
		document.getElementById("divDisplayImageToCharacterize");

	var imageToCharacterizeAsCanvas = 
		divDisplayImageToCharacterize.getElementsByTagName("canvas")[0];

	if (imageToCharacterizeAsCanvas == null)
	{
		alert("No image loaded!");
		return;
	}

	var inputColorBackground = 
		document.getElementById("inputColorBackground");
	var inputColorDifferenceThreshold = 
		document.getElementById("inputColorDifferenceThreshold");

	var colorBackground = inputColorBackground.value;
	var colorDifferenceThreshold = inputColorDifferenceThreshold.value;

	var imageCharacterizedAsCanvas = new ImageAutoCharacterizer().findCharactersInCanvas
	(
		imageToCharacterizeAsCanvas,
		colorBackground,
		colorDifferenceThreshold
	);
	imageCharacterizedAsCanvas.style = "border:1px solid";

	var divDisplayImageCharacterized = 
		document.getElementById("divDisplayImageCharacterized");
	divDisplayImageCharacterized.innerHTML = "";
	divDisplayImageCharacterized.appendChild(imageCharacterizedAsCanvas);
}

function inputImageToCharacterize_Changed(input)
{
	var file = input.files[0];
	var fileReader = new FileReader();
	fileReader.onload = function(eventFileLoaded)
	{
		var imageAsDataURL = eventFileLoaded.target.result;
		var imageAsDOMElement = document.createElement("img");
		imageAsDOMElement.onload = function(eventImageLoaded)
		{
			var imageAsCanvas = document.createElement("canvas");
			imageAsCanvas.style = "border:1px solid";
			imageAsCanvas.width = imageAsDOMElement.width;
			imageAsCanvas.height = imageAsDOMElement.height;

			var graphics = imageAsCanvas.getContext("2d");
			graphics.drawImage(imageAsDOMElement, 0, 0);

			imageAsCanvas.onmousedown = function(mouseEvent)
			{
				var x = mouseEvent.x;
				var y = mouseEvent.y;

				var pixelRGBA = graphics.getImageData
				(
					x, y, 1, 1
				).data;

				var pixelAsString = 
					+ pixelRGBA[0] + ","
					+ pixelRGBA[1] + ","
					+ pixelRGBA[2]

				var inputColorBackground = 
					document.getElementById("inputColorBackground");

				inputColorBackground.value = pixelAsString;
			}

			var divDisplayImageToCharacterize = document.getElementById
			(
				"divDisplayImageToCharacterize"
			);
			divDisplayImageToCharacterize.innerHTML = "";
			divDisplayImageToCharacterize.appendChild
			(
				imageAsCanvas
			);
		}
		imageAsDOMElement.src = imageAsDataURL;
	}
	fileReader.readAsDataURL(file);
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
}
{
	Bounds.prototype.size = function()
	{
		return this.max.clone().subtract(this.min);
	}
}

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

	Coords.prototype.dimension = function(dimensionIndex, valueToSet)
	{
		return (dimensionIndex == 0 ? this.x : this.y);
	}

	Coords.prototype.dimensionSet = function(dimensionIndex, valueToSet)
	{
		if (dimensionIndex == 0)
		{
			this.x = valueToSet;
		}
		else
		{
			this.y = valueToSet;
		}

		return this;
	}

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

function ImageAutoCharacterizer()
{
	// do nothing
}
{
	ImageAutoCharacterizer.prototype.findCharactersInCanvas = function
	(
		imageToFindCharactersInAsCanvas,
		colorBackground,
		colorDifferenceThreshold,
	)
	{
		// Draw the image itself.

		var imageCharacterizedAsCanvas = document.createElement("canvas");

		var imageSize = new Coords
		(
			imageToFindCharactersInAsCanvas.width,
			imageToFindCharactersInAsCanvas.height
		);

		imageCharacterizedAsCanvas.width = imageSize.x;
		imageCharacterizedAsCanvas.height = imageSize.y;

		graphics = imageCharacterizedAsCanvas.getContext("2d");
		graphics.drawImage
		(
			imageToFindCharactersInAsCanvas, 
			0, 0
		);

		// Locate the spaces between lines of text.

		var spacesBetweenLinesOfText = this.findSpacesInCanvasInBounds
		(
			imageToFindCharactersInAsCanvas,
			new Bounds(new Coords(0, 0), imageSize),
			colorBackground,
			colorDifferenceThreshold,
			1 // axisI = y
		);

		var lineGroups = 
		[
			spacesBetweenLinesOfText
		];

		for (var i = 0; i < spacesBetweenLinesOfText.length - 1; i++)
		{
			var lineTop = spacesBetweenLinesOfText[i];
			var lineBottom = spacesBetweenLinesOfText[i + 1];

			var spacesBetweenCharactersInLine = this.findSpacesInCanvasInBounds
			(
				imageToFindCharactersInAsCanvas,
				new Bounds(lineTop.fromPos, lineBottom.toPos),
				colorBackground,
				colorDifferenceThreshold,
				0 // axisI = x
			);

			lineGroups.push(spacesBetweenCharactersInLine);			
		}		

		// Draw all the lines.

		graphics.strokeStyle = "Cyan";

		for (var g = 0; g < lineGroups.length; g++)
		{
			var linesInGroup = lineGroups[g];

			for (var i = 0; i < linesInGroup.length; i++)
			{
				var line = linesInGroup[i];

				line.drawToGraphics(graphics);
			}
		}

		return imageCharacterizedAsCanvas;

	}

	ImageAutoCharacterizer.prototype.findSpacesInCanvasInBounds = function
	(	
		imageToSplitAsCanvas,
		bounds,
		colorBackground,
		colorDifferenceThreshold,
		axisI
	)
	{			
		var colorBackgroundRGB = colorBackground.split(",");

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

		var imageToSplitSize = new Coords
		(
			imageToSplitAsCanvas.width,
			imageToSplitAsCanvas.height
		);

		var pixelPos = new Coords();

		var linesOpen = [];

		var axisJ = 1 - axisI;

		var min = bounds.min;
		var max = bounds.max;

		var iMin = min.dimension(axisI);
		var iMax = max.dimension(axisI);

		var jMin = min.dimension(axisJ);
		var jMax = max.dimension(axisJ);

		for (var i = iMin; i < iMax; i++)
		{
			pixelPos.dimensionSet(axisI, i);

			var isLineOpen = true;
				
			for (var direction = 0; direction < 2; direction++)
			{
				var jStart = (direction == 0 ? jMin : jMax - 1);
				var jEnd = (direction == 0 ? jMax : jMin - 1);
				var jStep = (direction == 0 ? 1 : -1);

				for (var j = jStart; j != jEnd; j += jStep)
				{
 					pixelPos.dimensionSet(axisJ, j);
	
					var isPixelWithinThreshold = 
						this.isPixelWithinThreshold
						(
							graphics, 
							colorBackgroundRGB, 
							colorDifferenceThreshold, 
							pixelPos
						);

					if (isPixelWithinThreshold == false)
					{
						isLineOpen = false;

						break;
					}

				} // end for j

				if (isLineOpen == false)
				{
					break;
				}

			} // end for d

			if (isLineOpen == true)
			{
				var lineOpen = new Line
				(
					new Coords().dimensionSet
					(
						axisJ, jMin
					).dimensionSet
					(
						axisI, i
					),
					new Coords().dimensionSet
					(
						axisJ, jMax
					).dimensionSet
					(
						axisI, i
					)
				);

				linesOpen.push(lineOpen);
			}
		
		} // end for i

		var lineGroupCurrent = [];
		var lineGroups = [];
		var linePrev = null;

		for (var i = 0; i < linesOpen.length; i++)
		{
			var line = linesOpen[i];

			if (linePrev != null)
			{
				var distanceBetweenLineAndPrev = 
					line.fromPos.dimension(axisI) 
					- linePrev.fromPos.dimension(axisI);

				if (distanceBetweenLineAndPrev > 1)
				{
					lineGroups.push(lineGroupCurrent);
					lineGroupCurrent = [];	
				}
			}

			lineGroupCurrent.push(line);

			linePrev = line;
		}

		lineGroups.push(lineGroupCurrent);

		var linesSpaced = [];

		for (var g = 0; g < lineGroups.length; g++)
		{
			var lineGroup = lineGroups[g];

			var indexOfLineAtCenterOfGroup 
				= Math.floor(lineGroup.length / 2);

			var lineAtCenterOfGroup = lineGroup[indexOfLineAtCenterOfGroup];

			linesSpaced.push(lineAtCenterOfGroup);
		}

		var linesForSpaces = linesSpaced;

		return linesForSpaces;
	}

	ImageAutoCharacterizer.prototype.isPixelWithinThreshold = function
	(
		graphics, colorBackgroundRGB, colorDifferenceThreshold, pixelPos
	)
	{
		var isPixelWithinThreshold = false;

		var pixelRGB = graphics.getImageData
		(
			pixelPos.x, pixelPos.y, 1, 1
		).data;

		var pixelDifference = 0;
		var numberOfColorComponents = 3; // rgb

		for (var c = 0; c < numberOfColorComponents; c++)
		{
			var componentDifference = Math.abs
			(
				pixelRGB[c] - colorBackgroundRGB[c]
			);
			pixelDifference += componentDifference;
		}

		var isPixelWithinThreshold =  
			(pixelDifference <= colorDifferenceThreshold);

		return isPixelWithinThreshold;
	}
}

function Line(fromPos, toPos)
{
	this.fromPos = fromPos;
	this.toPos = toPos;
}
{
	Line.prototype.drawToGraphics = function(graphics)
	{
		graphics.beginPath();
		graphics.moveTo(this.fromPos.x, this.fromPos.y);
		graphics.lineTo(this.toPos.x, this.toPos.y);
		graphics.stroke();
	}
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , | Leave a comment

Drawing Rules Between Lines of Text in Javascript

The JavaScript program below will automatically draw rules between lines of text in a loaded image. It is intended as an early step in an optical character recognition system. To see it in action, copy it into a .html file and open that file in a web browser that runs JavaScript.


<html>
<body>

<!-- ui -->

<div id="divUI">

	<label><b>Image "Rulifier"</b></label>
	<p>Load a file, specify a background color, and click the button to add ruled lines to it.</p>
	
	<div>
		<label>Image to Add Ruled Lines to:</label>
		<input type="file" onchange="inputImageToRule_Changed(this);"></input>
		<div id="divDisplayImageToRule">[none]</div>
	</div>

	<div>
		</div>
			<label>Background Color RGB:</label>
			<input id="inputColorBackground" value="255,255,255"></input>
		</div>
		<div>
			<label>Color Difference Threshold:</label>
			<input id="inputColorDifferenceThreshold" value="0"></input>
		</div>
		<button onclick="buttonImageRule_Clicked();">Add Ruled Lines</button>
	</div>

	<div>
		<label>Image with Ruled Lines:</label>
		<div id="divDisplayImageRuled">[none]</div>
	</div>

</div>

<!-- ui ends-->

<script type="text/javascript">

// ui events

function buttonImageRule_Clicked()
{
	var divDisplayImageToRule = 
		document.getElementById("divDisplayImageToRule");

	var imageToRuleAsCanvas = 
		divDisplayImageToRule.getElementsByTagName("canvas")[0];

	if (imageToRuleAsCanvas == null)
	{
		alert("No image loaded!");
		return;
	}

	var inputColorBackground = 
		document.getElementById("inputColorBackground");
	var inputColorDifferenceThreshold = 
		document.getElementById("inputColorDifferenceThreshold");

	var colorBackground = inputColorBackground.value;
	var colorDifferenceThreshold = inputColorDifferenceThreshold.value;

	var imageRuledAsCanvas = new ImageAutoRulifier().ruleCanvas
	(
		imageToRuleAsCanvas,
		colorBackground,
		colorDifferenceThreshold
	);
	imageRuledAsCanvas.style = "border:1px solid";

	var divDisplayImageRuled = 
		document.getElementById("divDisplayImageRuled");
	divDisplayImageRuled.innerHTML = "";
	divDisplayImageRuled.appendChild(imageRuledAsCanvas);
}

function inputImageToRule_Changed(input)
{
	var file = input.files[0];
	var fileReader = new FileReader();
	fileReader.onload = function(eventFileLoaded)
	{
		var imageAsDataURL = eventFileLoaded.target.result;
		var imageAsDOMElement = document.createElement("img");
		imageAsDOMElement.onload = function(eventImageLoaded)
		{
			var imageAsCanvas = document.createElement("canvas");
			imageAsCanvas.style = "border:1px solid";
			imageAsCanvas.width = imageAsDOMElement.width;
			imageAsCanvas.height = imageAsDOMElement.height;

			var graphics = imageAsCanvas.getContext("2d");
			graphics.drawImage(imageAsDOMElement, 0, 0);

			imageAsCanvas.onmousedown = function(mouseEvent)
			{
				var x = mouseEvent.x;
				var y = mouseEvent.y;

				var pixelRGBA = graphics.getImageData
				(
					x, y, 1, 1
				).data;

				var pixelAsString = 
					+ pixelRGBA[0] + ","
					+ pixelRGBA[1] + ","
					+ pixelRGBA[2]

				var inputColorBackground = 
					document.getElementById("inputColorBackground");

				inputColorBackground.value = pixelAsString;
			}

			var divDisplayImageToRule = document.getElementById
			(
				"divDisplayImageToRule"
			);
			divDisplayImageToRule.innerHTML = "";
			divDisplayImageToRule.appendChild
			(
				imageAsCanvas
			);
		}
		imageAsDOMElement.src = imageAsDataURL;
	}
	fileReader.readAsDataURL(file);
}

// classes

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

	Coords.prototype.dimension = function(dimensionIndex, valueToSet)
	{
		var returnValue;

		if (valueToSet == null)		
		{
			returnValue = (dimensionIndex == 0 ? this.x : this.y);
		}
		else 
		{
			if (dimensionIndex == 0)
			{
				this.x = valueToSet;
			}
			else
			{
				this.y = valueToSet;
			}
		}

		return returnValue;
	}

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

function ImageAutoRulifier()
{
	// do nothing
}
{
	ImageAutoRulifier.prototype.ruleCanvas = function
	(
		imageToRuleAsCanvas,
		colorBackground,
		colorDifferenceThreshold
	)
	{			
		var colorBackgroundRGB = colorBackground.split(",");

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

		var imageToRuleSize = new Coords
		(
			imageToRuleAsCanvas.width,
			imageToRuleAsCanvas.height
		);

		var pixelPos = new Coords();

		var numberOfAxes = 2;

		var linesOpen = [];

		// hack - Start at 1 to just check the horizontal lines.
		for (var axis0 = 1; axis0 < numberOfAxes; axis0++)
		{
			var axis1 = 1 - axis0;

			var sizeAlongAxis0 = imageToRuleSize.dimension(axis0);
			var sizeAlongAxis1 = imageToRuleSize.dimension(axis1);

			for (var i = 0; i < sizeAlongAxis0; i++)
			{
				pixelPos.dimension(axis0, i);

				var isLineOpen = true;
				
				for (var direction = 0; direction < 2; direction++)
				{
					var jStart = (direction == 0 ? 0 : sizeAlongAxis1 - 1);
					var jEnd = (direction == 0 ? sizeAlongAxis1 : -1);
					var jStep = (direction == 0 ? 1 : -1);

					for (var j = jStart; j != jEnd; j += jStep)
					{
	 					pixelPos.dimension(axis1, j);
	
						var isPixelWithinThreshold = 
							this.ruleCanvas_IsPixelWithinThreshold
							(
								graphics, 
								colorBackgroundRGB, 
								colorDifferenceThreshold, 
								pixelPos
							);

						if (isPixelWithinThreshold == false)
						{
							isLineOpen = false;

							break;
						}

					} // end for j

					if (isLineOpen == false)
					{
						break;
					}

				} // end for d

				if (isLineOpen == true)
				{
					var lineOpen = new Line
					(
						new Coords(0, i),
						new Coords(sizeAlongAxis1, i)
					);

					linesOpen.push(lineOpen);
				}
		
			} // end for i

		} // end for a

		var lineGroupCurrent = [];
		var lineGroups = [];
		var linePrev = linesOpen[0];

		for (var i = 1; i < linesOpen.length; i++)
		{
			var line = linesOpen[i];

			var distanceBetweenLineAndPrev = 
				line.fromPos.y - linePrev.fromPos.y;

			if (distanceBetweenLineAndPrev > 1)
			{
				lineGroups.push(lineGroupCurrent);
				lineGroupCurrent = [];	
			}

			lineGroupCurrent.push(line);

			linePrev = line;
		}

		var linesSpaced = [];

		for (var g = 0; g < lineGroups.length; g++)
		{
			var lineGroup = lineGroups[g];

			var indexOfLineAtCenterOfGroup 
				= Math.floor(lineGroup.length / 2);

			var lineAtCenterOfGroup = lineGroup[indexOfLineAtCenterOfGroup];

			linesSpaced.push(lineAtCenterOfGroup);
		}

		var linesForRules = linesSpaced;

		var imageRuledAsCanvas = document.createElement("canvas");

		var imageRuledSize = imageToRuleSize;

		imageRuledAsCanvas.width = imageRuledSize.x;
		imageRuledAsCanvas.height = imageRuledSize.y;

		graphics = imageRuledAsCanvas.getContext("2d");
		graphics.drawImage
		(
			imageToRuleAsCanvas, 
			0, 0
		);

		graphics.strokeStyle = "Cyan";

		for (var i = 0; i < linesForRules.length; i++)
		{
			var line = linesForRules[i];
			graphics.beginPath();
			graphics.moveTo(line.fromPos.x, line.fromPos.y);
			graphics.lineTo(line.toPos.x, line.toPos.y);
			graphics.stroke();
		}

		return imageRuledAsCanvas;
	}

	ImageAutoRulifier.prototype.ruleCanvas_IsPixelWithinThreshold = function
	(
		graphics, colorBackgroundRGB, colorDifferenceThreshold, pixelPos
	)
	{
		var isPixelWithinThreshold = false;

		var pixelRGB = graphics.getImageData
		(
			pixelPos.x, pixelPos.y, 1, 1
		).data;

		var pixelDifference = 0;
		var numberOfColorComponents = 3; // rgb

		for (var c = 0; c < numberOfColorComponents; c++)
		{
			var componentDifference = Math.abs
			(
				pixelRGB[c] - colorBackgroundRGB[c]
			);
			pixelDifference += componentDifference;
		}

		var isPixelWithinThreshold =  
			(pixelDifference <= colorDifferenceThreshold);

		return isPixelWithinThreshold;
	}
}

function Line(fromPos, toPos)
{
	this.fromPos = fromPos;
	this.toPos = toPos;
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

An Image Auto-Cropper in JavaScript

The JavaScript program below allows the user to specify an input file, a background color, and a difference threshold, and will automatically crop the image to remove any pixels around its border that match the background color within the specified difference threshold. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2071/06/22 – The code has been updated to reuse the same code for scanning pixels along both the x and y axes, rather than simply using two different sets of nested loops with identical contents. For efficiency’s sake, the code also now scans each column or row of pixels from both sides, stopping when the first non-croppable pixel is found, rather than scanning the whole line of pixels including the non-croppable ones. A long comment describing this new scanning process in relative detail has also been added.


<html>
<body>

<!-- ui -->

<div id="divUI">

	<label><b>Image Autocropper</b></label>
	<p>Load an image file, click on it to set a background color, set a difference threshold, and click the Crop button.</p>
	
	<div>
		<label>Image to Crop:</label>
		<input type="file" onchange="inputImageToCrop_Changed(this);"></input>
		<div id="divDisplayImageToCrop">[none]</div>
	</div>

	<div>
		</div>
			<label>Background Color RGB:</label>
			<input id="inputColorToCrop" value="255,255,255"></input>
		</div>
		<div>
			<label>Color Difference Threshold:</label>
			<input id="inputColorDifferenceThreshold" value="0"></input>
		</div>
		<button onclick="buttonImageAutocrop_Clicked();">Crop</button>
	</div>

	<div>
		<label>Image Cropped:</label>
		<div id="divDisplayImageCropped">[none]</div>
	</div>

</div>

<!-- ui ends-->

<script type="text/javascript">

// ui events

function buttonImageAutocrop_Clicked()
{
	// todo - validation

	var divDisplayImageToCrop = 
		document.getElementById("divDisplayImageToCrop");
	var inputColorToCrop = 
		document.getElementById("inputColorToCrop");
	var inputColorDifferenceThreshold = 
		document.getElementById("inputColorDifferenceThreshold");

	var imageToCropAsCanvas = 
		divDisplayImageToCrop.getElementsByTagName("canvas")[0];

	var colorToCrop = inputColorToCrop.value;
	var colorDifferenceThreshold = inputColorDifferenceThreshold.value;

	var imageCroppedAsCanvas = new ImageAutocropper().cropCanvas
	(
		imageToCropAsCanvas,
		colorToCrop,
		colorDifferenceThreshold
	);
	imageCroppedAsCanvas.style = "border:1px solid";

	var divDisplayImageCropped = 
		document.getElementById("divDisplayImageCropped");
	divDisplayImageCropped.innerHTML = "";
	divDisplayImageCropped.appendChild(imageCroppedAsCanvas);
}

function inputImageToCrop_Changed(input)
{
	var file = input.files[0];
	var fileReader = new FileReader();
	fileReader.onload = function(eventFileLoaded)
	{
		var imageAsDataURL = eventFileLoaded.target.result;
		var imageAsDOMElement = document.createElement("img");
		imageAsDOMElement.onload = function(eventImageLoaded)
		{
			var imageAsCanvas = document.createElement("canvas");
			imageAsCanvas.style = "border:1px solid";
			imageAsCanvas.width = imageAsDOMElement.width;
			imageAsCanvas.height = imageAsDOMElement.height;

			var graphics = imageAsCanvas.getContext("2d");
			graphics.drawImage(imageAsDOMElement, 0, 0);

			imageAsCanvas.onmousedown = function(mouseEvent)
			{
				var x = mouseEvent.x;
				var y = mouseEvent.y;

				var pixelRGBA = graphics.getImageData
				(
					x, y, 1, 1
				).data;

				var pixelAsString = 
					+ pixelRGBA[0] + ","
					+ pixelRGBA[1] + ","
					+ pixelRGBA[2]

				var inputColorToCrop = 
					document.getElementById("inputColorToCrop");

				inputColorToCrop.value = pixelAsString;
			}

			var divDisplayImageToCrop = document.getElementById
			(
				"divDisplayImageToCrop"
			);
			divDisplayImageToCrop.innerHTML = "";
			divDisplayImageToCrop.appendChild
			(
				imageAsCanvas
			);
		}
		imageAsDOMElement.src = imageAsDataURL;
	}
	fileReader.readAsDataURL(file);
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
}
{
	Bounds.prototype.size = function()
	{
		return this.max.clone().subtract(this.min);
	}
}

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

	Coords.prototype.dimension = function(dimensionIndex, valueToSet)
	{
		var returnValue;

		if (valueToSet == null)		
		{
			returnValue = (dimensionIndex == 0 ? this.x : this.y);
		}
		else 
		{
			if (dimensionIndex == 0)
			{
				this.x = valueToSet;
			}
			else
			{
				this.y = valueToSet;
			}
		}

		return returnValue;
	}

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

function ImageAutocropper()
{
	// do nothing
}
{
	ImageAutocropper.prototype.cropCanvas = function
	(
		imageToCropAsCanvas,
		colorToCrop,
		colorDifferenceThreshold
	)
	{			
		var cropBounds = this.cropCanvas_FindBounds
		(
			imageToCropAsCanvas,
			colorToCrop,
			colorDifferenceThreshold
		);

		var imageCroppedAsCanvas = document.createElement("canvas");

		var imageCroppedSize = cropBounds.size();

		imageCroppedAsCanvas.width = imageCroppedSize.x;
		imageCroppedAsCanvas.height = imageCroppedSize.y;

		var graphics2 = imageCroppedAsCanvas.getContext("2d");
		graphics2.drawImage
		(
			imageToCropAsCanvas, 
			// source rectangle
			cropBounds.min.x, cropBounds.min.y,
			imageCroppedSize.x, imageCroppedSize.y,
			// target rectangle
			0, 0, 
			imageCroppedSize.x, imageCroppedSize.y
		);

		return imageCroppedAsCanvas
	}

	ImageAutocropper.prototype.cropCanvas_FindBounds = function
	(
		imageToCropAsCanvas,
		colorToCrop,
		colorDifferenceThreshold
	)
	{
		var colorToCropRGB = colorToCrop.split(",");

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

		var imageToCropSize = new Coords
		(
			imageToCropAsCanvas.width,
			imageToCropAsCanvas.height
		);

		var cropBounds = new Bounds
		(
			imageToCropSize.clone(),
			new Coords(0, 0)
		);

		// We start with the upper-left pixel,
		// and go down until we find a "non-croppable" pixel outside the threshold.
		// When that happens, we expand the top bound if necessary,
		// then jump to the bottom pixel in the column,
		// and proceed upward until, again, a pixel outside the threshold is found.
		// This time we expand the bottom bound if necessary.
		// Then we go to the top of the next column of pixels,
		// and repeat the top-down and bottom-up loops for each column,
		// expanding the bounds as necessary
		// so that they include all known non-croppable pixels.
		// Then we switch axes from top-to-bottom to left-to-right 
		// and repeat to set the left and right bounds.

		var pixelPos = new Coords();

		var numberOfAxes = 2;

		for (var a0 = 0; a0 < numberOfAxes; a0++)
		{
			var a1 = 1 - a0;

			var sizeAlongAxis0 = imageToCropSize.dimension(a0);
			var sizeAlongAxis1 = imageToCropSize.dimension(a1);

			for (var i = 0; i < sizeAlongAxis0; i++)
			{
				pixelPos.dimension(a0, i);

				for (var d = 0; d < 2; d++)
				{
					var jStart = (d == 0 ? 0 : sizeAlongAxis1 - 1);
					var jEnd = (d == 0 ? sizeAlongAxis1 : -1);
					var jStep = (d == 0 ? 1 : -1);

					for (var j = jStart; j != jEnd; j += jStep)
					{
	 					pixelPos.dimension(a1, j);
	
						var isPixelWithinThreshold = 
							this.cropCanvas_FindBounds_IsPixelWithinThreshold
							(
								graphics, 
								colorToCropRGB, 
								colorDifferenceThreshold, 
								pixelPos
							);

						if (isPixelWithinThreshold == false)
						{
							this.cropCanvas_FindBounds_BoundsExpand
							(
								cropBounds, pixelPos
							);

							break;
						}

					} // end for j

				} // end for d
		
			} // end for i

		} // end for a

		return cropBounds;
	}

	ImageAutocropper.prototype.cropCanvas_FindBounds_IsPixelWithinThreshold = function
	(
		graphics, colorToCropRGB, colorDifferenceThreshold, pixelPos
	)
	{
		var isPixelWithinThreshold = false;

		var pixelRGB = graphics.getImageData
		(
			pixelPos.x, pixelPos.y, 1, 1
		).data;

		var pixelDifference = 0;
		var numberOfColorComponents = 3; // rgb

		for (var c = 0; c < numberOfColorComponents; c++)
		{
			var componentDifference = Math.abs
			(
				pixelRGB[c] - colorToCropRGB[c]
			);
			pixelDifference += componentDifference;
		}

		var isPixelWithinThreshold =  
			(pixelDifference <= colorDifferenceThreshold);

		return isPixelWithinThreshold;
	}

	ImageAutocropper.prototype.cropCanvas_FindBounds_BoundsExpand = function
	(
		cropBounds, pixelPos
	)
	{
		if (pixelPos.x < cropBounds.min.x)
		{
			cropBounds.min.x = pixelPos.x;
		}

		if (pixelPos.x > cropBounds.max.x)
		{
			cropBounds.max.x = pixelPos.x;
		}

		if (pixelPos.y < cropBounds.min.y)
		{
			cropBounds.min.y = pixelPos.y;
		}

		if (pixelPos.y > cropBounds.max.y)
		{
			cropBounds.max.y = pixelPos.y;
		}

		return cropBounds;
	}

}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment