diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee
index c84b60be61..d231e57428 100644
--- a/cms/static/coffee/spec/main.coffee
+++ b/cms/static/coffee/spec/main.coffee
@@ -34,6 +34,7 @@ requirejs.config({
"sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1",
"squire": "xmodule_js/common_static/js/vendor/Squire",
"jasmine-jquery": "xmodule_js/common_static/js/vendor/jasmine-jquery",
+ "jasmine-imagediff": "xmodule_js/common_static/js/vendor/jasmine-imagediff",
"jasmine-stealth": "xmodule_js/common_static/js/vendor/jasmine-stealth",
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
@@ -151,6 +152,9 @@ requirejs.config({
"jasmine-jquery": {
deps: ["jasmine"]
},
+ "jasmine-imagediff": {
+ deps: ["jasmine"]
+ },
"jasmine-stealth": {
deps: ["jasmine"]
},
diff --git a/cms/static/js_test.yml b/cms/static/js_test.yml
index f33b46a25e..cdf8d9cec2 100644
--- a/cms/static/js_test.yml
+++ b/cms/static/js_test.yml
@@ -47,6 +47,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/Squire.js
- xmodule_js/common_static/js/vendor/jasmine-jquery.js
- xmodule_js/common_static/js/vendor/jasmine-stealth.js
+ - xmodule_js/common_static/js/vendor/jasmine-imagediff.js
- xmodule_js/common_static/js/vendor/jasmine.async.js
- xmodule_js/common_static/js/vendor/jquery.maskedinput.min.js
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
diff --git a/cms/static/js_test_squire.yml b/cms/static/js_test_squire.yml
index 2b37e4792f..a3c7313ee8 100644
--- a/cms/static/js_test_squire.yml
+++ b/cms/static/js_test_squire.yml
@@ -46,6 +46,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/Squire.js
- xmodule_js/common_static/js/vendor/jasmine-jquery.js
- xmodule_js/common_static/js/vendor/jasmine-stealth.js
+ - xmodule_js/common_static/js/vendor/jasmine-imagediff.js
- xmodule_js/common_static/js/vendor/jasmine.async.js
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/src/xmodule.js
diff --git a/common/lib/xmodule/xmodule/js/js_test.yml b/common/lib/xmodule/xmodule/js/js_test.yml
index 7fb5699ecf..0a0699df9c 100644
--- a/common/lib/xmodule/xmodule/js/js_test.yml
+++ b/common/lib/xmodule/xmodule/js/js_test.yml
@@ -36,6 +36,7 @@ lib_paths:
- common_static/coffee/src/ajax_prefix.js
- common_static/coffee/src/logger.js
- common_static/js/vendor/jasmine-jquery.js
+ - common_static/js/vendor/jasmine-imagediff.js
- common_static/js/vendor/require.js
- RequireJS-namespace-undefine.js
- common_static/js/vendor/jquery.min.js
diff --git a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee
index 7debc16af1..45dc73824b 100644
--- a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee
@@ -303,6 +303,114 @@ describe 'Problem', ->
expect($('input#1_2_1_choiceinput_2bc').attr('disabled')).not.toEqual('disabled')
expect($('input#1_2_1').attr('disabled')).not.toEqual('disabled')
+ describe 'imageinput', ->
+ imageinput_html='''
+
+ '''
+ states = [
+ {
+ desc: 'rectangle is drawn correctly',
+ data: {'rectangle': '(10,10)-(30,30)'}
+ },
+ {
+ desc: 'region is drawn correctly',
+ data: {'regions': '[[10,10],[30,30],[70,30],[20,30]]'}
+ },
+ {
+ desc: 'mixed shapes are drawn correctly',
+ data: {
+ 'rectangle': '(10,10)-(30,30);(5,5)-(20,20)',
+ 'regions': '''[
+ [[50,50],[40,40],[70,30],[50,70]],
+ [[90,95],[95,95],[90,70],[70,70]]
+ ]'''
+ }
+ },
+ ]
+
+ beforeEach ->
+ @problem = new Problem($('.xblock-student_view'))
+ @problem.el.prepend imageinput_html
+
+ stubRequest = (data) =>
+ spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
+ callback answers: "1_2_1": data
+
+ getImage = (coords, c_width, c_height) =>
+ types =
+ rectangle: (coords) =>
+ reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/
+ rects = coords.replace(/\s*/g, '').split(/;/)
+
+ $.each rects, (index, rect) =>
+ abs = Math.abs
+ points = reg.exec(rect)
+ if points
+ # width
+ width = abs(points[3] - points[1])
+ # height
+ height = abs(points[4] - points[2])
+
+ ctx.rect(points[1], points[2], width, height)
+
+ ctx.stroke()
+ ctx.fill()
+
+ regions: (coords) =>
+ parseCoords = (coords) =>
+ reg = JSON.parse(coords)
+
+ if typeof reg[0][0][0] == "undefined"
+ reg = [reg]
+
+ return reg
+
+ $.each parseCoords(coords), (index, region) =>
+ ctx.beginPath()
+ $.each region, (index, point) =>
+ if index is 0
+ ctx.moveTo(point[0], point[1])
+ else
+ ctx.lineTo(point[0], point[1]);
+
+ ctx.closePath()
+ ctx.stroke()
+ ctx.fill()
+
+ canvas = document.createElement('canvas')
+ canvas.width = c_width or 100
+ canvas.height = c_height or 100
+
+ if canvas.getContext
+ ctx = canvas.getContext('2d')
+ else
+ return console.log 'Canvas is not supported.'
+
+ ctx.fillStyle = 'rgba(255,255,255,.3)';
+ ctx.strokeStyle = "#FF0000";
+ ctx.lineWidth = "2";
+
+ $.each coords, (key, value) =>
+ types[key](value) if types[key]?
+
+ return canvas
+
+ $.each states, (index, state) =>
+ it state.desc, ->
+ stubRequest(state.data)
+ @problem.show()
+ img = getImage(state.data)
+
+ expect(img).toImageDiffEqual($('canvas')[0])
+
describe 'when the answers are already shown', ->
beforeEach ->
@problem.el.addClass 'showed'
@@ -409,4 +517,3 @@ describe 'Problem', ->
expect(@problem.answers).toEqual "input_1_1=one&input_1_2=two"
-
diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee
index 026ff1133b..851692d87a 100644
--- a/common/lib/xmodule/xmodule/js/spec/helper.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee
@@ -162,6 +162,8 @@ beforeEach ->
toBeInArray: (array) ->
return $.inArray(@.actual, array) > -1
+ @addMatchers imagediff.jasmine
+
# Stub jQuery.cookie
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0'
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index edd781f8a3..697983e299 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -506,18 +506,18 @@ class @Problem
parseCoords = (coords) =>
reg = JSON.parse(coords)
- if typeof reg[0][0] is undefined
+ if typeof reg[0][0][0] == "undefined"
reg = [reg]
return reg
- $.each parseCoords(coords), (index, regions) =>
+ $.each parseCoords(coords), (index, region) =>
ctx.beginPath()
- $.each regions, (index, points) =>
+ $.each region, (index, point) =>
if index is 0
- ctx.moveTo(points[0], points[1])
+ ctx.moveTo(point[0], point[1])
else
- ctx.lineTo(points[0], points[1]);
+ ctx.lineTo(point[0], point[1]);
ctx.closePath()
ctx.stroke()
diff --git a/common/static/js/vendor/jasmine-imagediff.js b/common/static/js/vendor/jasmine-imagediff.js
new file mode 100644
index 0000000000..6460e59dc5
--- /dev/null
+++ b/common/static/js/vendor/jasmine-imagediff.js
@@ -0,0 +1,382 @@
+// js-imagediff 1.0.3
+// (c) 2011-2012 Carl Sutherland, Humble Software
+// Distributed under the MIT License
+// For original source and documentation visit:
+// http://www.github.com/HumbleSoftware/js-imagediff
+
+(function (name, definition) {
+ var root = this;
+ if (typeof module !== 'undefined') {
+ var Canvas = require('canvas');
+ module.exports = definition(root, name, Canvas);
+ } else if (typeof define === 'function' && typeof define.amd === 'object') {
+ define(definition);
+ } else {
+ root[name] = definition(root, name);
+ }
+})('imagediff', function (root, name, Canvas) {
+
+ var
+ TYPE_ARRAY = /\[object Array\]/i,
+ TYPE_CANVAS = /\[object (Canvas|HTMLCanvasElement)\]/i,
+ TYPE_CONTEXT = /\[object CanvasRenderingContext2D\]/i,
+ TYPE_IMAGE = /\[object (Image|HTMLImageElement)\]/i,
+ TYPE_IMAGE_DATA = /\[object ImageData\]/i,
+
+ UNDEFINED = 'undefined',
+
+ canvas = getCanvas(),
+ context = canvas.getContext('2d'),
+ previous = root[name],
+ imagediff, jasmine;
+
+ // Creation
+ function getCanvas (width, height) {
+ var
+ canvas = Canvas ?
+ new Canvas() :
+ document.createElement('canvas');
+ if (width) canvas.width = width;
+ if (height) canvas.height = height;
+ return canvas;
+ }
+ function getImageData (width, height) {
+ canvas.width = width;
+ canvas.height = height;
+ context.clearRect(0, 0, width, height);
+ return context.createImageData(width, height);
+ }
+
+
+ // Type Checking
+ function isImage (object) {
+ return isType(object, TYPE_IMAGE);
+ }
+ function isCanvas (object) {
+ return isType(object, TYPE_CANVAS);
+ }
+ function isContext (object) {
+ return isType(object, TYPE_CONTEXT);
+ }
+ function isImageData (object) {
+ return !!(object &&
+ isType(object, TYPE_IMAGE_DATA) &&
+ typeof(object.width) !== UNDEFINED &&
+ typeof(object.height) !== UNDEFINED &&
+ typeof(object.data) !== UNDEFINED);
+ }
+ function isImageType (object) {
+ return (
+ isImage(object) ||
+ isCanvas(object) ||
+ isContext(object) ||
+ isImageData(object)
+ );
+ }
+ function isType (object, type) {
+ return typeof (object) === 'object' && !!Object.prototype.toString.apply(object).match(type);
+ }
+
+
+ // Type Conversion
+ function copyImageData (imageData) {
+ var
+ height = imageData.height,
+ width = imageData.width,
+ data = imageData.data,
+ newImageData, newData, i;
+
+ canvas.width = width;
+ canvas.height = height;
+ newImageData = context.getImageData(0, 0, width, height);
+ newData = newImageData.data;
+
+ for (i = imageData.data.length; i--;) {
+ newData[i] = data[i];
+ }
+
+ return newImageData;
+ }
+ function toImageData (object) {
+ if (isImage(object)) { return toImageDataFromImage(object); }
+ if (isCanvas(object)) { return toImageDataFromCanvas(object); }
+ if (isContext(object)) { return toImageDataFromContext(object); }
+ if (isImageData(object)) { return object; }
+ }
+ function toImageDataFromImage (image) {
+ var
+ height = image.height,
+ width = image.width;
+ canvas.width = width;
+ canvas.height = height;
+ context.clearRect(0, 0, width, height);
+ context.drawImage(image, 0, 0);
+ return context.getImageData(0, 0, width, height);
+ }
+ function toImageDataFromCanvas (canvas) {
+ var
+ height = canvas.height,
+ width = canvas.width,
+ context = canvas.getContext('2d');
+ return context.getImageData(0, 0, width, height);
+ }
+ function toImageDataFromContext (context) {
+ var
+ canvas = context.canvas,
+ height = canvas.height,
+ width = canvas.width;
+ return context.getImageData(0, 0, width, height);
+ }
+ function toCanvas (object) {
+ var
+ data = toImageData(object),
+ canvas = getCanvas(data.width, data.height),
+ context = canvas.getContext('2d');
+
+ context.putImageData(data, 0, 0);
+ return canvas;
+ }
+
+
+ // ImageData Equality Operators
+ function equalWidth (a, b) {
+ return a.width === b.width;
+ }
+ function equalHeight (a, b) {
+ return a.height === b.height;
+ }
+ function equalDimensions (a, b) {
+ return equalHeight(a, b) && equalWidth(a, b);
+ }
+ function equal (a, b, tolerance) {
+
+ var
+ aData = a.data,
+ bData = b.data,
+ length = aData.length,
+ i;
+
+ tolerance = tolerance || 0;
+
+ if (!equalDimensions(a, b)) return false;
+ for (i = length; i--;) if (aData[i] !== bData[i] && Math.abs(aData[i] - bData[i]) > tolerance) return false;
+
+ return true;
+ }
+
+
+ // Diff
+ function diff (a, b) {
+ return (equalDimensions(a, b) ? diffEqual : diffUnequal)(a, b);
+ }
+ function diffEqual (a, b) {
+
+ var
+ height = a.height,
+ width = a.width,
+ c = getImageData(width, height), // c = a - b
+ aData = a.data,
+ bData = b.data,
+ cData = c.data,
+ length = cData.length,
+ row, column,
+ i, j, k, v;
+
+ for (i = 0; i < length; i += 4) {
+ cData[i] = Math.abs(aData[i] - bData[i]);
+ cData[i+1] = Math.abs(aData[i+1] - bData[i+1]);
+ cData[i+2] = Math.abs(aData[i+2] - bData[i+2]);
+ cData[i+3] = Math.abs(255 - Math.abs(aData[i+3] - bData[i+3]));
+ }
+
+ return c;
+ }
+ function diffUnequal (a, b) {
+
+ var
+ height = Math.max(a.height, b.height),
+ width = Math.max(a.width, b.width),
+ c = getImageData(width, height), // c = a - b
+ aData = a.data,
+ bData = b.data,
+ cData = c.data,
+ rowOffset,
+ columnOffset,
+ row, column,
+ i, j, k, v;
+
+
+ for (i = cData.length - 1; i > 0; i = i - 4) {
+ cData[i] = 255;
+ }
+
+ // Add First Image
+ offsets(a);
+ for (row = a.height; row--;){
+ for (column = a.width; column--;) {
+ i = 4 * ((row + rowOffset) * width + (column + columnOffset));
+ j = 4 * (row * a.width + column);
+ cData[i+0] = aData[j+0]; // r
+ cData[i+1] = aData[j+1]; // g
+ cData[i+2] = aData[j+2]; // b
+ // cData[i+3] = aData[j+3]; // a
+ }
+ }
+
+ // Subtract Second Image
+ offsets(b);
+ for (row = b.height; row--;){
+ for (column = b.width; column--;) {
+ i = 4 * ((row + rowOffset) * width + (column + columnOffset));
+ j = 4 * (row * b.width + column);
+ cData[i+0] = Math.abs(cData[i+0] - bData[j+0]); // r
+ cData[i+1] = Math.abs(cData[i+1] - bData[j+1]); // g
+ cData[i+2] = Math.abs(cData[i+2] - bData[j+2]); // b
+ }
+ }
+
+ // Helpers
+ function offsets (imageData) {
+ rowOffset = Math.floor((height - imageData.height) / 2);
+ columnOffset = Math.floor((width - imageData.width) / 2);
+ }
+
+ return c;
+ }
+
+
+ // Validation
+ function checkType () {
+ var i;
+ for (i = 0; i < arguments.length; i++) {
+ if (!isImageType(arguments[i])) {
+ throw {
+ name : 'ImageTypeError',
+ message : 'Submitted object was not an image.'
+ };
+ }
+ }
+ }
+
+
+ // Jasmine Matchers
+ function get (element, content) {
+ element = document.createElement(element);
+ if (element && content) {
+ element.innerHTML = content;
+ }
+ return element;
+ }
+
+ jasmine = {
+
+ toBeImageData : function () {
+ return imagediff.isImageData(this.actual);
+ },
+
+ toImageDiffEqual : function (expected, tolerance) {
+
+ if (typeof (document) !== UNDEFINED) {
+ this.message = function () {
+ var
+ div = get('div'),
+ a = get('div', '
Actual:
'),
+ b = get('div', 'Expected:
'),
+ c = get('div', 'Diff:
'),
+ diff = imagediff.diff(this.actual, expected),
+ canvas = getCanvas(),
+ context;
+
+ canvas.height = diff.height;
+ canvas.width = diff.width;
+
+ div.style.overflow = 'hidden';
+ a.style.float = 'left';
+ b.style.float = 'left';
+ c.style.float = 'left';
+
+ context = canvas.getContext('2d');
+ context.putImageData(diff, 0, 0);
+
+ a.appendChild(toCanvas(this.actual));
+ b.appendChild(toCanvas(expected));
+ c.appendChild(canvas);
+
+ div.appendChild(a);
+ div.appendChild(b);
+ div.appendChild(c);
+
+ return [
+ div,
+ "Expected not to be equal."
+ ];
+ };
+ }
+
+ return imagediff.equal(this.actual, expected, tolerance);
+ }
+ };
+
+
+ // Image Output
+ function imageDataToPNG (imageData, outputFile, callback) {
+
+ var
+ canvas = toCanvas(imageData),
+ base64Data,
+ decodedImage;
+
+ callback = callback || Function;
+
+ base64Data = canvas.toDataURL().replace(/^data:image\/\w+;base64,/,"");
+ decodedImage = new Buffer(base64Data, 'base64');
+ require('fs').writeFile(outputFile, decodedImage, callback);
+ }
+
+
+ // Definition
+ imagediff = {
+
+ createCanvas : getCanvas,
+ createImageData : getImageData,
+
+ isImage : isImage,
+ isCanvas : isCanvas,
+ isContext : isContext,
+ isImageData : isImageData,
+ isImageType : isImageType,
+
+ toImageData : function (object) {
+ checkType(object);
+ if (isImageData(object)) { return copyImageData(object); }
+ return toImageData(object);
+ },
+
+ equal : function (a, b, tolerance) {
+ checkType(a, b);
+ a = toImageData(a);
+ b = toImageData(b);
+ return equal(a, b, tolerance);
+ },
+ diff : function (a, b) {
+ checkType(a, b);
+ a = toImageData(a);
+ b = toImageData(b);
+ return diff(a, b);
+ },
+
+ jasmine : jasmine,
+
+ // Compatibility
+ noConflict : function () {
+ root[name] = previous;
+ return imagediff;
+ }
+ };
+
+ if (typeof module !== 'undefined') {
+ imagediff.imageDataToPNG = imageDataToPNG;
+ }
+
+ return imagediff;
+});
diff --git a/common/static/js_test.yml b/common/static/js_test.yml
index 117fd7f662..81879153f3 100644
--- a/common/static/js_test.yml
+++ b/common/static/js_test.yml
@@ -30,6 +30,7 @@ prepend_path: common/static
lib_paths:
- js/vendor/jquery.min.js
- js/vendor/jasmine-jquery.js
+ - js/vendor/jasmine-imagediff.js
- js/vendor/underscore-min.js
- js/vendor/backbone-min.js
- js/vendor/jquery.timeago.js
diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml
index a49b2fde81..a7520a8fc1 100644
--- a/lms/static/js_test.yml
+++ b/lms/static/js_test.yml
@@ -31,6 +31,7 @@ lib_paths:
- xmodule_js/common_static/coffee/src/ajax_prefix.js
- xmodule_js/common_static/coffee/src/logger.js
- xmodule_js/common_static/js/vendor/jasmine-jquery.js
+ - xmodule_js/common_static/js/vendor/jasmine-imagediff.js
- xmodule_js/common_static/js/vendor/require.js
- js/RequireJS-namespace-undefine.js
- xmodule_js/common_static/js/vendor/jquery.min.js