├── LICENSE ├── README.md ├── img ├── source-image.jpg ├── wm-1.png ├── wm-2.png └── wm-3.png ├── index.html ├── lib └── watermark.js ├── procedural-watermark-sample.js ├── ready.js └── result-screen-shot.png /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Carlos Cabo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # watermark-js 2 | Small JS library to watermarks pictures using JS and HTML5 Canvas element. 3 | 4 | 5 | 6 | **This library is primary intended for back-end usage, allowing the backend users to add watermarks / badges to the images in a non-removable way (the watermarks are _rendered_ over the image itself).** In front-end there are several ways of **simulate a _watermark_** over an image without the need of using this library. 7 | 8 | ## Requirements 9 | 10 | ```` 11 | JQuery 1.5+ 12 | Web browser with HTML5 Canvas support (IE9+) 13 | ```` 14 | 15 | ## Basic usage 16 | 17 | ````javascript 18 | my_watermarked = new Watermark(); // Create new object instance 19 | 20 | my_watermarked 21 | .setPicture('img/source-image.jpg') // Base picture, url or data-url 22 | .addWatermark('img/wm-1.png') // Url or data-url 23 | .render( function(){ 24 | // Do something when watermarking ends 25 | }); 26 | ```` 27 | 28 | ## IMPORTANT(1): CORS 29 | If you get a message in the brower's console about **«A "tainted" canvas»**, you are trying to use images with the _CORS enabled_ [read a brief explanation at MDN](https://developer.mozilla.org/es/docs/Web/HTML/Imagen_con_CORS_habilitado). 30 | 31 | If you are going to work with uploaded **images stored in a different server domain, you will need to enable CORS configuration in the third-party storage**, in order to let the library work with images. 32 | For example, if you are using Amazon S3 storage for your images, this is the [CORS documentation](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html#how-do-i-enable-cors). 33 | 34 | ## IMPORTANT(2): The .render() callback 35 | The **watermarking proccess is asyncronous**, so if you want to access / use the resulting watermarked images you must do it inside the `.render()` method, passing a _callback_ function that will be executed **once the watermarking is finished**. 36 | 37 | ## Creating several watermarked thumbnails 38 | You can pass an optional array of `widths` to the `.setPicture()` method to create several sizes from the original base image, this eases the task of creating several sizes of the watermarked image automatically. 39 | 40 | ````javascript 41 | my_watermarked 42 | .setPicture('img/source-image.jpg', [640, 320, 240]) // Array of thumbs widths 43 | //... 44 | ```` 45 | 46 | ## Watermark images options 47 | The watermark images can have several optional settings 48 | 49 | ````javascript 50 | .addWatermark('img/wm-3.png', // Image url or data-url 51 | { 52 | position: [1,1], // Default is [0.5, 0.5] 53 | scale: 2.0, // Default is 1.0 54 | opacity: 0.5 // Default is 1.0 55 | } 56 | ) 57 | ```` 58 | 59 | `position` indicates the position of the watermark **relative to the base image**, first component of the array is the horizontal position, and second one the vertical position, both in a range of [0-1]: 60 | 61 | ````javascript 62 | // Positions 63 | // left top -> [0, 0] 64 | // left center -> [0, 0.5] 65 | // left bottom -> [0, 1] 66 | // right top -> [1, 0] 67 | // right center -> [1, 0.5] 68 | // right bottom -> [1, 1] 69 | // center top -> [0.5, 0] 70 | // center center -> [0.5, 0.5] 71 | // center bottom -> [0.5, 1] 72 | ```` 73 | 74 | `scale` sets an enlargement / reduction to the watermark. So if you want the watermark be **half the size the original image you must set this value `0.5`**. Remember that enlarging images can produce _blurry_ results. 75 | 76 | ## Accessing the resulting watermarked images 77 | 78 | You have two methods to retrieve the resulting watermarked images, both return **an array of elements**: 79 | 80 | ````javascript 81 | my_watermarked.getImgs( format, quality ); // Returns array of s 82 | my_watermarked.getDataUrls( format, quality ); // Returns array of data-urls 83 | // format: 'image/png' (default) / 'image/jpeg' 84 | // quality: float in range [ 0-1 ] ( 1 is best quality ) 85 | ```` 86 | 87 | Sample usage: 88 | 89 | ````javascript 90 | // Gets all the resulting images in PNG format 91 | var resulting_imgs = my_watermarked.getImgs( 'image/png' ); 92 | // Add all of them to the element 93 | $.each( resulting_imgs, function(idx, item) { 94 | $('body').append( $(item) ); 95 | }); 96 | 97 | // Get all the resulting data urls in Jpeg 90% quality 98 | var resulting_data_urls = my_watermarked.getDataUrls( 'image/jpeg', 0.9 ); 99 | console.log(resulting_data_urls); 100 | ```` 101 | 102 | ## Clear watermarks 103 | 104 | Clear watermark configurations and results in case you want to make a new fresh watermark. 105 | 106 | ````javascript 107 | .clearWatermarks(); 108 | ```` 109 | 110 | ## Advanced usage 111 | As both `.setPicture()` and `.addWatermark()` accept a data-url image as parameter you can build complex / dynamic watermarks **passing a functión that return data-url as result**. Take a look to the `procedural-watermark-sample.js` included in the repo to see the sample function that creates the price badge . 112 | 113 | Following the demo code, with all the avaliable options. 114 | 115 | ````javascript 116 | my_watermarked = new Watermark(); 117 | 118 | my_watermarked 119 | .setPicture('img/source-image.jpg', [400, 250]) 120 | .addWatermark('img/wm-1.png', 121 | { 122 | position: [0,0] 123 | } 124 | ) 125 | .addWatermark('img/wm-2.png') 126 | .addWatermark('img/wm-3.png', 127 | { 128 | position: [1,1], 129 | scale: 2.0, 130 | opacity: 0.5 131 | } 132 | ) 133 | .addWatermark( 134 | proceduralWatermark( 'DESDE', '99', ',99€'), 135 | { 136 | position: [1,0] 137 | } 138 | ) 139 | .render( function(){ 140 | 141 | // var resulting_canvas = wm.getCanvases(); 142 | // $.each( resulting_canvas, function(idx, item) { 143 | // $('body').append( $(item) ); 144 | // }); 145 | 146 | var resulting_imgs = my_watermarked.getImgs( 'image/png' ); 147 | $.each( resulting_imgs, function(idx, item) { 148 | $('body').append( $(item) ); 149 | }); 150 | 151 | var resulting_data_urls = my_watermarked.getDataUrls( 'image/jpeg', 0.9 ); 152 | console.log(resulting_data_urls); 153 | 154 | }); // render callback 155 | ```` 156 | 157 | ## Utils 158 | ````javascript 159 | // Return the data-URL of image in selector 160 | my_watermarked.getDataUrlFromImg( $('selector') ); 161 | ```` 162 | 163 | ## Changelog 164 | * 0.2 Avoid web browser cache issues when requesting CORS images 165 | * 0.1 Initial release 166 | 167 | ## TO-DO (or not ;) 168 | - Add vertical constraints to the thumbnails widths 169 | - Add a `fitWidth` to the watermark options... 170 | -------------------------------------------------------------------------------- /img/source-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carloscabo/watermark-js/6a960d51c4a1ae728bb02383dd25d1d4fb6b31a1/img/source-image.jpg -------------------------------------------------------------------------------- /img/wm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carloscabo/watermark-js/6a960d51c4a1ae728bb02383dd25d1d4fb6b31a1/img/wm-1.png -------------------------------------------------------------------------------- /img/wm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carloscabo/watermark-js/6a960d51c4a1ae728bb02383dd25d1d4fb6b31a1/img/wm-2.png -------------------------------------------------------------------------------- /img/wm-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carloscabo/watermark-js/6a960d51c4a1ae728bb02383dd25d1d4fb6b31a1/img/wm-3.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Watermark JS 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/watermark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * watermark-js V0.2 by Carlos Cabo 2016 3 | * https://github.com/carloscabo/watermark-js 4 | */ 5 | 6 | // data URI - MDN https://developer.mozilla.org/en-US/docs/data_URIs 7 | // The "data" URL scheme: http://tools.ietf.org/html/rfc2397 8 | // Valid URL Characters: http://tools.ietf.org/html/rfc2396#section2 9 | 10 | // Positions 11 | // left top -> 0, 0 12 | // left center -> 0, 0.5 13 | // left bottom -> 0, 1 14 | // right top -> 1, 0 15 | // right center -> 1, 0.5 16 | // right bottom -> 1, 1 17 | // center top -> 0.5, 0 18 | // center center -> 0.5, 0.5 19 | // center bottom -> 0.5, 1 20 | 21 | var Watermark = (function() { 22 | 23 | function Constructor( user_settings ) { 24 | this.version = 0.1; 25 | 26 | // Internal data 27 | this.data = { 28 | picture: { 29 | sizes: null 30 | }, // Source picture 31 | results: [], // Resulting watermarker images 32 | watermarks: [], // Watermarks to be applied 33 | pending_watermarks: 0, 34 | callback: null 35 | }; 36 | 37 | // Default settings 38 | this.settings = {}; 39 | $.extend(true, this.settings, user_settings); 40 | 41 | } 42 | 43 | Constructor.prototype = { 44 | 45 | // Sets base pìcture to work with 46 | setPicture: function( url_or_data, sizes ) { 47 | var 48 | _t = this.data; 49 | _t.picture.url = this.addAntiCacheParam( url_or_data ); 50 | if (typeof sizes !== 'undefined') _t.picture.sizes = sizes; 51 | return this; // Chainning 52 | }, // setPicture 53 | 54 | // Adds a watermark element that will be rendered ove the base picture 55 | // when the .render() methos is called 56 | addWatermark: function( url_or_data, user_options ) { 57 | var 58 | _t = this, 59 | wm = {}, 60 | default_options = { 61 | position: [0.5, 0.5], 62 | scale: 1.0, 63 | opacity: 1.0 64 | }; 65 | 66 | _t.data.pending_watermarks++; 67 | 68 | wm.url = _t.addAntiCacheParam( url_or_data ); 69 | wm.options = $.extend(default_options, user_options); 70 | 71 | _t.data.watermarks.push( wm ); 72 | 73 | return this; 74 | }, 75 | 76 | // Clear watermark configurations and results 77 | // in case you want to make a fresh watermark 78 | clearWatermarks: function() { 79 | var _t = this; 80 | _t.data.pending_watermarks = 0; 81 | _t.data.watermarks.length = 0; // faster than = [] 82 | _t.data.results.length = 0; // faster than = [] 83 | }, 84 | 85 | // Creates a canvas an return an object with 86 | // .canvas and .ctx (context) 87 | createCanvas: function( img, sx, sy, sw, sh, dx, dy, dw, dh ) { 88 | var 89 | objs = {}; 90 | objs.canvas = document.createElement('canvas'); 91 | objs.canvas.width = dw; 92 | objs.canvas.height = dh; 93 | objs.ctx = objs.canvas.getContext('2d'); 94 | objs.ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh); 95 | return objs; 96 | }, // createCanvas 97 | 98 | // Starts the process of creating the base picture canvas and thumbs 99 | // Once done it renders the watermarks over the pictures 100 | // Finally launches the callback function 101 | render: function( callback ) { 102 | var 103 | _t = this, 104 | $img = $(''); 105 | 106 | _t.data.callback = callback; 107 | 108 | // The crossOrigin attribute is a CORS settings attribute. 109 | // Its purpose is to allow images from third-party sites that allow 110 | // cross-origin access to be used with canvas. 111 | // Remember enabled cross-origin access in the third-party site, 112 | // for example if you are using amazon S3 for storage: 113 | // http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html#how-do-i-enable-cors 114 | $img[0].crossOrigin = "Anonymous"; 115 | $img.on('load', function() { 116 | console.log('Source picture loaded'); 117 | var 118 | img = this, 119 | // Exact copy of picture 120 | picture = _t.createCanvas( img, 0, 0, img.width, img.height, 0, 0, img.width, img.height ); 121 | // picture.pos = _t.calculatePositions( img.width, img.height ); 122 | _t.data.results.push( picture ); 123 | 124 | // $('body').append( picture.canvas ); 125 | 126 | // Create thumbs 127 | if ( _t.data.picture.sizes !== null) { 128 | for (var i = 0; i < _t.data.picture.sizes.length; i++) { 129 | var 130 | w = _t.data.picture.sizes[i], 131 | h = parseInt( (img.height / img.width) * w, 10 ); 132 | picture = _t.createCanvas( img, 0, 0, img.width, img.height, 0, 0, w, h ); 133 | _t.data.results.push( picture ); 134 | // $('body').append( picture.canvas ); 135 | // console.log(_t.data.watermarks); 136 | // console.log(w, h); 137 | } 138 | } 139 | _t.renderWatermarks(); 140 | }).attr('src', _t.data.picture.url); 141 | }, 142 | 143 | renderWatermarks: function () { 144 | var 145 | _t = this; 146 | 147 | for (var i = 0; i < _t.data.watermarks.length; i++) { 148 | var 149 | wm = _t.data.watermarks[i], 150 | $img = $(''); 151 | 152 | // The crossOrigin attribute is a CORS settings attribute. 153 | // Its purpose is to allow images from third-party sites that allow 154 | // cross-origin access to be used with canvas. 155 | // Remember enabled cross-origin access in the third-party site, 156 | // for example if you are using amazon S3 for storage: 157 | // http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html#how-do-i-enable-cors 158 | $img[0].crossOrigin = "Anonymous"; 159 | $img.on('load', function() { 160 | 161 | var 162 | wm_img = this, 163 | wm_obj = _t.createCanvas( wm_img, 0, 0, wm_img.width, wm_img.height, 0, 0, wm_img.width, wm_img.height ), 164 | options= $(this).data('options'), 165 | scale = options.scale, 166 | position = options.position, 167 | w = wm_img.width * scale, 168 | h = wm_img.height * scale; 169 | 170 | // $('body').append( wm_obj.canvas ); 171 | for (var j = 0; j < _t.data.results.length; j++) { 172 | // _t.data.results[j]; 173 | _t.data.results[j].ctx.globalAlpha = options.opacity; 174 | _t.data.results[j].ctx.drawImage( 175 | wm_obj.canvas, 176 | ( _t.data.results[j].canvas.width - w ) * position[0], 177 | ( _t.data.results[j].canvas.height - h ) * position[1], 178 | w, 179 | h 180 | ); 181 | // $('body').append( _t.data.results[j].canvas ); 182 | } 183 | _t.data.pending_watermarks--; 184 | if (_t.data.pending_watermarks === 0) { 185 | _t.data.callback(); 186 | } 187 | }).data( 'options', wm.options ).attr('src', wm.url); 188 | } 189 | }, 190 | 191 | // Returns array of elements 192 | getCanvas: function () { 193 | var 194 | _t = this.data.results, 195 | canvas = []; 196 | for (var i = 0; i < _t.length; i++) { 197 | canvas.push(_t[i].canvas); 198 | } 199 | return canvas; 200 | }, 201 | 202 | // Returns array of data_urls 203 | getDataUrls: function ( filetype, quality ) { 204 | if ( typeof filetype === 'undefined') filetype = 'image/png'; 205 | if ( typeof quality === 'undefined') quality = 1.0; 206 | var 207 | data_urls = [], 208 | canvas = this.getCanvas(); 209 | for (var i = 0; i < canvas.length; i++) { 210 | data_urls.push( 211 | canvas[i].toDataURL( filetype, quality) 212 | ); 213 | } 214 | return data_urls; 215 | }, 216 | 217 | // Returns array of elements 218 | getImgs: function ( filetype, quality ) { 219 | if ( typeof filetype === 'undefined') filetype = 'image/png'; 220 | if ( typeof quality === 'undefined') quality = 1.0; 221 | var 222 | imgs = [], 223 | canvas = this.getCanvas(); 224 | for (var i = 0; i < canvas.length; i++) { 225 | var 226 | $img = $(''); 227 | imgs.push( 228 | $img.attr('src', canvas[i].toDataURL( filetype, quality)) 229 | ); 230 | } 231 | return imgs; 232 | }, 233 | 234 | // Utils 235 | getDataUrlFromImg: function ( img ) { 236 | var 237 | canvas = document.createElement('canvas'); 238 | // If is JQuery object get DOM element 239 | if (img instanceof jQuery) img = img[0]; 240 | canvas.width = img.naturalWidth; 241 | canvas.height = img.naturalHeight; 242 | canvas.getContext('2d').drawImage(img, 0, 0); 243 | return canvas.toDataURL('image/png'); 244 | }, 245 | 246 | // Seems that in some situations loading images from cross-domain resources 247 | // like S3, even setting CORS correctly gives troubles with the web browser 248 | // cache. To avoid this will add a timestamp to all the images urls to 249 | // avoid cache issues 250 | addAntiCacheParam: function( url_or_data ) { 251 | var 252 | is_dataurl_regex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i; 253 | // Is DataURL, we do nothing 254 | if ( url_or_data.match( is_dataurl_regex ) ) return url_or_data; 255 | // Is a regualar URL, we add a timestamp 256 | url_or_data += url_or_data.match( /[\?]/g ) ? '&' : '?' + 't=' + Date.now(); 257 | return url_or_data; 258 | } 259 | 260 | }; 261 | 262 | return Constructor; 263 | })(); 264 | -------------------------------------------------------------------------------- /procedural-watermark-sample.js: -------------------------------------------------------------------------------- 1 | function proceduralWatermark( top_text, price_int, price_float ) { 2 | 3 | var 4 | canvas = document.createElement('canvas'), 5 | ctx = canvas.getContext('2d'), 6 | cx = 0, 7 | cy = 0; 8 | 9 | canvas.width = 100; 10 | canvas.height = 100; 11 | cx = canvas.width / 2; 12 | cy = canvas.height / 2; 13 | 14 | ctx.beginPath(); 15 | ctx.arc(cx, cy, 40, 0, 2 * Math.PI, false); 16 | ctx.fillStyle = '#ee0000'; 17 | ctx.fill(); 18 | 19 | ctx.font = 'Bold 12px Arial, sans-serif'; 20 | ctx.textAlign = 'center'; 21 | ctx.fillStyle = '#ffffff'; 22 | ctx.textBaseline = 'middle'; 23 | ctx.fillText(top_text, cx, 32); 24 | 25 | ctx.font = 'Bold 30px Arial, sans-serif'; 26 | ctx.textAlign = 'left'; 27 | ctx.fillStyle = '#ffffff'; 28 | ctx.textBaseline = 'alphabetic'; 29 | var tm = ctx.measureText(price_int); // TextMetrics 30 | ctx.fillText(price_int, (cx - tm.width) - 4 , 64); 31 | 32 | ctx.font = 'Bold 20px Arial, sans-serif'; 33 | ctx.textAlign = 'right'; 34 | ctx.fillStyle = '#ffffff'; 35 | ctx.textBaseline = 'alphabetic'; 36 | var tm = ctx.measureText( price_float); // TextMetrics 37 | ctx.fillText( price_float, (cx + tm.width) - 6 , 64); 38 | 39 | return canvas.toDataURL(); 40 | } 41 | -------------------------------------------------------------------------------- /ready.js: -------------------------------------------------------------------------------- 1 | var my_watermarked; 2 | 3 | $(document).ready(function() { 4 | // La magia aquí 5 | 6 | $('') 7 | .attr('src', proceduralWatermark( 'DESDE', '99', ',99€')) 8 | .appendTo( $('body') ); 9 | 10 | my_watermarked = new Watermark(); 11 | 12 | my_watermarked 13 | .setPicture('img/source-image.jpg', [400, 250]) 14 | .addWatermark('img/wm-1.png', 15 | { 16 | position: [0,0] 17 | } 18 | ) 19 | .addWatermark('img/wm-2.png') 20 | .addWatermark('img/wm-3.png', 21 | { 22 | position: [1,1], 23 | scale: 2.0, 24 | opacity: 0.5 25 | } 26 | ) 27 | .addWatermark( 28 | proceduralWatermark( 'DESDE', '99', ',99€'), 29 | { 30 | position: [1,0] 31 | } 32 | ) 33 | .render( function(){ 34 | 35 | // var resulting_canvas = wm.getCanvases(); 36 | // $.each( resulting_canvas, function(idx, item) { 37 | // $('body').append( $(item) ); 38 | // }); 39 | 40 | var resulting_imgs = my_watermarked.getImgs( 'image/png' ); 41 | $.each( resulting_imgs, function(idx, item) { 42 | $('body').append( $(item) ); 43 | }); 44 | 45 | var resulting_data_urls = my_watermarked.getDataUrls( 'image/jpeg', 0.9 ); 46 | console.log(resulting_data_urls); 47 | 48 | }); // render callback 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /result-screen-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carloscabo/watermark-js/6a960d51c4a1ae728bb02383dd25d1d4fb6b31a1/result-screen-shot.png --------------------------------------------------------------------------------