diff --git a/.gitignore b/.gitignore index 8336d14..651d780 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist docs node_modules +.DS_Store diff --git a/build/steganography.js b/build/steganography.js index 8df0f8e..b6e76c2 100644 --- a/build/steganography.js +++ b/build/steganography.js @@ -1,5 +1,5 @@ /* - * steganography.js v1.0.2 2016-04-23 + * steganography.js v1.0.3 2017-09-22 * * Copyright (C) 2012 Peter Eigenschink (http://www.peter-eigenschink.at/) * Dual-licensed under MIT and Beerware license. @@ -59,10 +59,10 @@ var util = { "loadImg": function(url) { var image = new Image(); image.src = url; - while(image.hasOwnProperty('complete') && !image.complete) {} return image; } }; + Cover.prototype.config = { "t": 3, "threshold": 1, @@ -94,8 +94,13 @@ Cover.prototype.getHidingCapacity = function(image, options) { return t*width*height/codeUnitSize >> 0; }; Cover.prototype.encode = function(message, image, options) { + // Handle image url if(image.length) { image = util.loadImg(image); + } else if(image.src) { + image = util.loadImg(image.src); + } else if(!(image instanceof HTMLImageElement)) { + throw new Error('IllegalInput: The input image is neither an URL string nor an image.'); } options = options || {}; @@ -108,7 +113,7 @@ Cover.prototype.encode = function(message, image, options) { args = options.args || config.args, messageDelimiter = options.messageDelimiter || config.messageDelimiter; - if(!t || t < 1 || t > 7) throw "Error: Parameter t = " + t + " is not valid: 0 < t < 8"; + if(!t || t < 1 || t > 7) throw new Error('IllegalOptions: Parameter t = " + t + " is not valid: 0 < t < 8'); var shadowCanvas = document.createElement('canvas'), shadowCtx = shadowCanvas.getContext('2d'); @@ -141,7 +146,10 @@ Cover.prototype.encode = function(message, image, options) { dec = message.charCodeAt(i) || 0; curOverlapping = (overlapping*i)%t; if(curOverlapping > 0 && oldDec) { + // Mask for the new character, shifted with the count of overlapping bits mask = Math.pow(2,t-curOverlapping) - 1; + // Mask for the old character, i.e. the t-curOverlapping bits on the right + // of that character oldMask = Math.pow(2, codeUnitSize) * (1 - Math.pow(2, -curOverlapping)); left = (dec & mask) << curOverlapping; right = (oldDec & oldMask) >> (codeUnitSize - curOverlapping); @@ -189,7 +197,7 @@ Cover.prototype.encode = function(message, image, options) { } for(i=offset*4; i<(offset+qS.length)*4 && i 7) throw "Error: Parameter t = " + t + " is not valid: 0 < t < 8"; - + if(!t || t < 1 || t > 7) throw new Error('IllegalOptions: Parameter t = " + t + " is not valid: 0 < t < 8'); + var shadowCanvas = document.createElement('canvas'), shadowCtx = shadowCanvas.getContext('2d'); @@ -306,5 +320,6 @@ Cover.prototype.decode = function(image, options) { return message; }; + return new Cover(); }); \ No newline at end of file diff --git a/build/steganography.min.js b/build/steganography.min.js index 1a83e15..83d59d7 100644 --- a/build/steganography.min.js +++ b/build/steganography.min.js @@ -1,7 +1,7 @@ /* - * steganography.js v1.0.2 2016-04-23 + * steganography.js v1.0.3 2017-09-22 * * Copyright (C) 2012 Peter Eigenschink (http://www.peter-eigenschink.at/) * Dual-licensed under MIT and Beerware license. */ -!function(a,b,c){"undefined"!=typeof module&&module.exports?module.exports=c():"function"==typeof define&&define.amd?define(c):b[a]=c()}("steg",this,function(){var a=function(){},b={isPrime:function(a){if(isNaN(a)||!isFinite(a)||a%1||2>a)return!1;if(a%2===0)return 2===a;if(a%3===0)return 3===a;for(var b=Math.sqrt(a),c=5;b>=c;c+=6){if(a%c===0)return!1;if(a%(c+2)===0)return!1}return!0},findNextPrime:function(a){for(var c=a;!0;c+=1)if(b.isPrime(c))return c},sum:function(a,b,c){var d=0;c=c||{};for(var e=c.start||0;b>e;e+=c.inc||1)d+=a(e)||0;return 0===d&&c.defValue?c.defValue:d},product:function(a,b,c){var d=1;c=c||{};for(var e=c.start||0;b>e;e+=c.inc||1)d*=a(e)||1;return 1===d&&c.defValue?c.defValue:d},createArrayFromArgs:function(a,b,c){for(var d=new Array(c-1),e=0;c>e;e+=1)d[e]=a(e>=b?e+1:e);return d},loadImg:function(a){var b=new Image;for(b.src=a;b.hasOwnProperty("complete")&&!b.complete;);return b}};return a.prototype.config={t:3,threshold:1,codeUnitSize:16,args:function(a){return a+1},messageDelimiter:function(a,b){for(var c=new Array(3*b),d=0;de&&d;e+=1)d=d&&255===a[b+4*e];return d}},a.prototype.getHidingCapacity=function(a,b){b=b||{};var c=this.config,d=b.width||a.width,e=b.height||a.height,f=b.t||c.t,g=b.codeUnitSize||c.codeUnitSize;return f*d*e/g>>0},a.prototype.encode=function(a,c,d){c.length&&(c=b.loadImg(c)),d=d||{};var e=this.config,f=d.t||e.t,g=d.threshold||e.threshold,h=d.codeUnitSize||e.codeUnitSize,i=b.findNextPrime(Math.pow(2,f)),j=d.args||e.args,k=d.messageDelimiter||e.messageDelimiter;if(!f||1>f||f>7)throw"Error: Parameter t = "+f+" is not valid: 0 < t < 8";var l=document.createElement("canvas"),m=l.getContext("2d");l.style.display="none",l.width=d.width||c.width,l.height=d.height||c.height,d.height&&d.width?m.drawImage(c,0,0,d.width,d.height):m.drawImage(c,0,0);var n,o,p,q,r,s,t,u,v,w,x=m.getImageData(0,0,l.width,l.height),y=x.data,z=h/f>>0,A=h%f,B=[];for(v=0;v<=a.length;v+=1){if(s=a.charCodeAt(v)||0,t=A*v%f,t>0&&o){if(u=Math.pow(2,f-t)-1,p=Math.pow(2,h)*(1-Math.pow(2,-t)),q=(s&u)<>h-t,B.push(q+r),vw;w+=1)n=s&u,B.push(n>>(w-1)*f+(f-t)),u<<=f;A*(v+1)%f===0?(u=Math.pow(2,h)*(1-Math.pow(2,-f)),n=s&u,B.push(n>>h-f)):f>=A*(v+1)%f+(f-t)&&(n=s&u,B.push(n>>(z-1)*f+(f-t)))}}else if(vw;w+=1)n=s&u,B.push(n>>w*f),u<<=f;o=s}var C,D,E,F,G,H=k(B,g);for(C=0;4*(C+g)<=y.length&&C+g<=B.length;C+=g){for(G=[],v=0;g>v&&v+Cw&&we||e>7)throw"Error: Parameter t = "+e+" is not valid: 0 < t < 8";var j=document.createElement("canvas"),k=j.getContext("2d");j.style.display="none",j.width=c.width||a.width,j.height=c.width||a.height,c.height&&c.width?k.drawImage(a,0,0,c.width,c.height):k.drawImage(a,0,0);var l,m,n=k.getImageData(0,0,j.width,j.height),o=n.data,p=[];if(1===f)for(l=3,m=!1;!m&&l=g&&(q+=String.fromCharCode(r&t),s%=g,r=p[l]>>e-s);return 0!==r&&(q+=String.fromCharCode(r&t)),q},new a}); \ No newline at end of file +!function(a,b,c){"undefined"!=typeof module&&module.exports?module.exports=c():"function"==typeof define&&define.amd?define(c):b[a]=c()}("steg",this,function(){var a=function(){},b={isPrime:function(a){if(isNaN(a)||!isFinite(a)||a%1||2>a)return!1;if(a%2===0)return 2===a;if(a%3===0)return 3===a;for(var b=Math.sqrt(a),c=5;b>=c;c+=6){if(a%c===0)return!1;if(a%(c+2)===0)return!1}return!0},findNextPrime:function(a){for(var c=a;!0;c+=1)if(b.isPrime(c))return c},sum:function(a,b,c){var d=0;c=c||{};for(var e=c.start||0;b>e;e+=c.inc||1)d+=a(e)||0;return 0===d&&c.defValue?c.defValue:d},product:function(a,b,c){var d=1;c=c||{};for(var e=c.start||0;b>e;e+=c.inc||1)d*=a(e)||1;return 1===d&&c.defValue?c.defValue:d},createArrayFromArgs:function(a,b,c){for(var d=new Array(c-1),e=0;c>e;e+=1)d[e]=a(e>=b?e+1:e);return d},loadImg:function(a){var b=new Image;return b.src=a,b}};return a.prototype.config={t:3,threshold:1,codeUnitSize:16,args:function(a){return a+1},messageDelimiter:function(a,b){for(var c=new Array(3*b),d=0;de&&d;e+=1)d=d&&255===a[b+4*e];return d}},a.prototype.getHidingCapacity=function(a,b){b=b||{};var c=this.config,d=b.width||a.width,e=b.height||a.height,f=b.t||c.t,g=b.codeUnitSize||c.codeUnitSize;return f*d*e/g>>0},a.prototype.encode=function(a,c,d){if(c.length)c=b.loadImg(c);else if(c.src)c=b.loadImg(c.src);else if(!(c instanceof HTMLImageElement))throw new Error("IllegalInput: The input image is neither an URL string nor an image.");d=d||{};var e=this.config,f=d.t||e.t,g=d.threshold||e.threshold,h=d.codeUnitSize||e.codeUnitSize,i=b.findNextPrime(Math.pow(2,f)),j=d.args||e.args,k=d.messageDelimiter||e.messageDelimiter;if(!f||1>f||f>7)throw new Error('IllegalOptions: Parameter t = " + t + " is not valid: 0 < t < 8');var l=document.createElement("canvas"),m=l.getContext("2d");l.style.display="none",l.width=d.width||c.width,l.height=d.height||c.height,d.height&&d.width?m.drawImage(c,0,0,d.width,d.height):m.drawImage(c,0,0);var n,o,p,q,r,s,t,u,v,w,x=m.getImageData(0,0,l.width,l.height),y=x.data,z=h/f>>0,A=h%f,B=[];for(v=0;v<=a.length;v+=1){if(s=a.charCodeAt(v)||0,t=A*v%f,t>0&&o){if(u=Math.pow(2,f-t)-1,p=Math.pow(2,h)*(1-Math.pow(2,-t)),q=(s&u)<>h-t,B.push(q+r),vw;w+=1)n=s&u,B.push(n>>(w-1)*f+(f-t)),u<<=f;A*(v+1)%f===0?(u=Math.pow(2,h)*(1-Math.pow(2,-f)),n=s&u,B.push(n>>h-f)):f>=A*(v+1)%f+(f-t)&&(n=s&u,B.push(n>>(z-1)*f+(f-t)))}}else if(vw;w+=1)n=s&u,B.push(n>>w*f),u<<=f;o=s}var C,D,E,F,G,H=k(B,g);for(C=0;4*(C+g)<=y.length&&C+g<=B.length;C+=g){for(G=[],v=0;g>v&&v+Cw&&we||e>7)throw new Error('IllegalOptions: Parameter t = " + t + " is not valid: 0 < t < 8');var j=document.createElement("canvas"),k=j.getContext("2d");j.style.display="none",j.width=c.width||a.width,j.height=c.width||a.height,c.height&&c.width?k.drawImage(a,0,0,c.width,c.height):k.drawImage(a,0,0);var l,m,n=k.getImageData(0,0,j.width,j.height),o=n.data,p=[];if(1===f)for(l=3,m=!1;!m&&l=g&&(q+=String.fromCharCode(r&t),s%=g,r=p[l]>>e-s);return 0!==r&&(q+=String.fromCharCode(r&t)),q},new a}); \ No newline at end of file diff --git a/package.json b/package.json index f416bcb..f9f6fb9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "type": "git", "url": "https://github.com/petereigenschink/steganography.js" }, - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "devDependencies": { "grunt": "0.4.5", diff --git a/src/decode.js b/src/decode.js index fd3d24a..a7bd762 100644 --- a/src/decode.js +++ b/src/decode.js @@ -1,20 +1,25 @@ Cover.prototype.decode = function(image, options) { + // Handle image url if(image.length) { image = util.loadImg(image); + } else if(image.src) { + image = util.loadImg(image.src); + } else if(!(image instanceof HTMLImageElement)) { + throw new Error('IllegalInput: The input image is neither an URL string nor an image.'); } options = options || {}; var config = this.config; - + var t = options.t || config.t, threshold = options.threshold || config.threshold, codeUnitSize = options.codeUnitSize || config.codeUnitSize, prime = util.findNextPrime(Math.pow(2, t)), - args = options.args || config.args, + args = options.args || config.args, messageCompleted = options.messageCompleted || config.messageCompleted; - if(!t || t < 1 || t > 7) throw "Error: Parameter t = " + t + " is not valid: 0 < t < 8"; - + if(!t || t < 1 || t > 7) throw new Error('IllegalOptions: Parameter t = " + t + " is not valid: 0 < t < 8'); + var shadowCanvas = document.createElement('canvas'), shadowCtx = shadowCanvas.getContext('2d'); @@ -100,4 +105,4 @@ Cover.prototype.decode = function(image, options) { if(charCode !== 0) message += String.fromCharCode(charCode & mask); return message; -}; \ No newline at end of file +}; diff --git a/src/encode.js b/src/encode.js index c1aa95c..d8e8866 100644 --- a/src/encode.js +++ b/src/encode.js @@ -1,6 +1,11 @@ Cover.prototype.encode = function(message, image, options) { + // Handle image url if(image.length) { image = util.loadImg(image); + } else if(image.src) { + image = util.loadImg(image.src); + } else if(!(image instanceof HTMLImageElement)) { + throw new Error('IllegalInput: The input image is neither an URL string nor an image.'); } options = options || {}; @@ -13,7 +18,7 @@ Cover.prototype.encode = function(message, image, options) { args = options.args || config.args, messageDelimiter = options.messageDelimiter || config.messageDelimiter; - if(!t || t < 1 || t > 7) throw "Error: Parameter t = " + t + " is not valid: 0 < t < 8"; + if(!t || t < 1 || t > 7) throw new Error('IllegalOptions: Parameter t = " + t + " is not valid: 0 < t < 8'); var shadowCanvas = document.createElement('canvas'), shadowCtx = shadowCanvas.getContext('2d'); @@ -46,7 +51,10 @@ Cover.prototype.encode = function(message, image, options) { dec = message.charCodeAt(i) || 0; curOverlapping = (overlapping*i)%t; if(curOverlapping > 0 && oldDec) { + // Mask for the new character, shifted with the count of overlapping bits mask = Math.pow(2,t-curOverlapping) - 1; + // Mask for the old character, i.e. the t-curOverlapping bits on the right + // of that character oldMask = Math.pow(2, codeUnitSize) * (1 - Math.pow(2, -curOverlapping)); left = (dec & mask) << curOverlapping; right = (oldDec & oldMask) >> (codeUnitSize - curOverlapping); @@ -94,7 +102,7 @@ Cover.prototype.encode = function(message, image, options) { } for(i=offset*4; i<(offset+qS.length)*4 && i> 0; }; Cover.prototype.encode = function(message, image, options) { + // Handle image url if(image.length) { image = util.loadImg(image); + } else if(image.src) { + image = util.loadImg(image.src); + } else if(!(image instanceof HTMLImageElement)) { + throw new Error('IllegalInput: The input image is neither an URL string nor an image.'); } options = options || {}; @@ -108,7 +113,7 @@ Cover.prototype.encode = function(message, image, options) { args = options.args || config.args, messageDelimiter = options.messageDelimiter || config.messageDelimiter; - if(!t || t < 1 || t > 7) throw "Error: Parameter t = " + t + " is not valid: 0 < t < 8"; + if(!t || t < 1 || t > 7) throw new Error('IllegalOptions: Parameter t = " + t + " is not valid: 0 < t < 8'); var shadowCanvas = document.createElement('canvas'), shadowCtx = shadowCanvas.getContext('2d'); @@ -141,7 +146,10 @@ Cover.prototype.encode = function(message, image, options) { dec = message.charCodeAt(i) || 0; curOverlapping = (overlapping*i)%t; if(curOverlapping > 0 && oldDec) { + // Mask for the new character, shifted with the count of overlapping bits mask = Math.pow(2,t-curOverlapping) - 1; + // Mask for the old character, i.e. the t-curOverlapping bits on the right + // of that character oldMask = Math.pow(2, codeUnitSize) * (1 - Math.pow(2, -curOverlapping)); left = (dec & mask) << curOverlapping; right = (oldDec & oldMask) >> (codeUnitSize - curOverlapping); @@ -189,7 +197,7 @@ Cover.prototype.encode = function(message, image, options) { } for(i=offset*4; i<(offset+qS.length)*4 && i 7) throw "Error: Parameter t = " + t + " is not valid: 0 < t < 8"; - + if(!t || t < 1 || t > 7) throw new Error('IllegalOptions: Parameter t = " + t + " is not valid: 0 < t < 8'); + var shadowCanvas = document.createElement('canvas'), shadowCtx = shadowCanvas.getContext('2d'); @@ -306,5 +320,6 @@ Cover.prototype.decode = function(image, options) { return message; }; + return new Cover(); }); \ No newline at end of file diff --git a/test/spec/steganography.spec.js b/test/spec/steganography.spec.js index 97d72ad..e3c8d2e 100755 --- a/test/spec/steganography.spec.js +++ b/test/spec/steganography.spec.js @@ -9,7 +9,7 @@ describe('steganography.js', function(){ } - describe('Encode/Decode consistency', function(){ + describe('Encode/Decode utf8 consistency', function(){ function encodeDecode(cb, config) { var img = new Image(); img.onload = function() { @@ -25,7 +25,7 @@ describe('steganography.js', function(){ img.src = resources.img.cover; } - it('is given using default config', function (done) { + it('is given for the message using default config', function (done) { encodeDecode(function(msg, readMsg) { expect(readMsg).toEqual(msg); done(); @@ -38,6 +38,51 @@ describe('steganography.js', function(){ expect(steg.encode).not.toBeUndefined(); }); + it('conserves image quality', function (done) { + var img = new Image(); + img.onload = function() { + var [originalWidth, originalHeight] = [img.width, img.height]; + var msg = readJSON(resources.json.utf8); + var dataURLWithImgCover = steg.encode(msg, img); + + var stegImg = new Image(); + stegImg.onload = function() { + var [stegWidth, stegHeight] = [stegImg.width, stegImg.height]; + + expect(originalWidth).toEqual(stegWidth); + expect(originalHeight).toEqual(stegHeight); + + done(); + } + stegImg.src = dataURLWithImgCover; + }; + img.src = resources.img.cover; + }); + + it('conserves image quality with resized DOM element', function (done) { + var img = new Image(); + img.style.width = '290px'; + img.style.height = 'auto'; + img.onload = function() { + var [originalWidth, originalHeight] = [img.width, img.height]; + document.body.appendChild(img); + var msg = readJSON(resources.json.utf8); + var dataURLWithImgCover = steg.encode(msg, img); + + var stegImg = new Image(); + stegImg.onload = function() { + var [stegWidth, stegHeight] = [stegImg.width, stegImg.height]; + + expect(originalWidth).toEqual(stegWidth); + expect(originalHeight).toEqual(stegHeight); + + done(); + } + stegImg.src = dataURLWithImgCover; + }; + img.src = resources.img.cover; + }); + it('works with URL', function(done) { var img = new Image(); img.onload = function() { @@ -50,7 +95,7 @@ describe('steganography.js', function(){ img.src = resources.img.cover; }); - it('works with base 64 data URL', function(done) { + xit('works with base 64 data URL', function(done) { var img = new Image(); img.onload = function() { var canvas = document.createElement('canvas'); @@ -66,18 +111,36 @@ describe('steganography.js', function(){ }; img.src = resources.img.cover; }); + + it('throws an error for non-string, non-image objects', function() { + expect(function() { steg.encode('Test', {}) }).toThrowError('IllegalInput: The input image is neither an URL string nor an image.'); + }) }); describe('Decode', function(){ it('is defined', function () { expect(steg.decode).not.toBeUndefined(); }); + + it('throws an error for non-string, non-image objects', function() { + expect(function() { steg.encode('Test', {}) }).toThrowError('IllegalInput: The input image is neither an URL string nor an image.'); + }) }); describe('Capacity', function(){ it('is defined', function () { expect(steg.getHidingCapacity).not.toBeUndefined(); }); + + it('works with default settings', function(done) { + var img = new Image(); + img.onload = function() { + var capacity = steg.getHidingCapacity(img); + expect(capacity).toEqual(18026) + done(); + }; + img.src = resources.img.cover; + }); }); describe('Config', function(){ @@ -86,4 +149,4 @@ describe('steganography.js', function(){ }); }); -}); \ No newline at end of file +});