├── README.md ├── main.css ├── main.js ├── smartcrop-wordpress.php └── smartcrop.js /README.md: -------------------------------------------------------------------------------- 1 | # smartcrop-wordpress 2 | A WordPress plugin that enables developers to use the fantastic [smartcrop.js](https://github.com/jwagner/smartcrop.js) functionality via one simple function. 3 | 4 | Based on and using the fantastic work of [@jwagner](https://github.com/jwagner) :heart:. 5 | 6 | ## Usage 7 | Simply replace the WordPress function `the_post_thumbnail()` with `the_smart_post_thumbnail()` - the arguments are the same as the original. -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | .smartcrop { 2 | overflow: hidden; 3 | } 4 | 5 | .smartcrop img { 6 | visibility: hidden; 7 | } 8 | 9 | .smartcrop canvas { 10 | opacity: 0; 11 | transition: 1s opacity ease-in; 12 | } 13 | 14 | .smartcrop.cropped img { 15 | display: none; 16 | } 17 | 18 | .smartcrop.cropped canvas { 19 | opacity: 1; 20 | } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(window).load(function(){ 3 | $('.smartcrop').each(function(){ 4 | var t = $(this), 5 | img = t.find('img'), 6 | width = t.attr('data-width'), 7 | height = t.attr('data-height'); 8 | 9 | SmartCrop.crop(img[0], {width: width, height: height}, function(result){ 10 | var crop = result.topCrop, 11 | canvas = t.find('canvas')[0], 12 | ctx = canvas.getContext('2d'); 13 | 14 | canvas.width = width; 15 | canvas.height = height; 16 | ctx.drawImage(img[0], crop.x, crop.y, crop.width, crop.height, 0, 0, canvas.width, canvas.height); 17 | 18 | img.after(canvas).closest('.smartcrop').addClass('cropped'); 19 | }); 20 | }); 21 | }); 22 | })(jQuery) -------------------------------------------------------------------------------- /smartcrop-wordpress.php: -------------------------------------------------------------------------------- 1 | $_wp_additional_image_sizes[ $_size ]['width'], 25 | 'height' => $_wp_additional_image_sizes[ $_size ]['height'], 26 | 'crop' => $_wp_additional_image_sizes[ $_size ]['crop'], 27 | ); 28 | } 29 | } 30 | 31 | return $sizes; 32 | } 33 | 34 | function sc_get_image_size( $size ) { 35 | $sizes = sc_get_image_sizes(); 36 | 37 | if ( isset( $sizes[ $size ] ) ) { 38 | return $sizes[ $size ]; 39 | } 40 | 41 | return false; 42 | } 43 | 44 | function the_smart_post_thumbnail( $size = 'thumbnail', $attr = '' ){ 45 | $the_size = sc_get_image_size( $size ); 46 | $the_thumbnail = get_the_post_thumbnail( null, 'full', $attr ); 47 | 48 | if('full' == $size){ 49 | echo $the_thumbnail; 50 | return; 51 | } 52 | 53 | $html = '
'; 54 | $html .= $the_thumbnail; 55 | $html .= ''; 56 | $html .= '
'; 57 | 58 | echo $html; 59 | } 60 | 61 | function sc_add_scripts() { 62 | wp_enqueue_script('smartcrop', plugins_url('/smartcrop.js', __FILE__), null ); 63 | wp_enqueue_script('smartcrop_main_js', plugins_url('/main.js', __FILE__), array('jquery', 'smartcrop') ); 64 | } 65 | add_action( 'wp_enqueue_scripts', 'sc_add_scripts'); 66 | 67 | function sc_add_styles() { 68 | wp_enqueue_style('smartcrop_main_css', plugins_url('/main.css', __FILE__), null ); 69 | } 70 | add_action( 'wp_enqueue_scripts', 'sc_add_styles'); -------------------------------------------------------------------------------- /smartcrop.js: -------------------------------------------------------------------------------- 1 | /** smart-crop.js 2 | * A javascript library implementing content aware image cropping 3 | * 4 | * Copyright (C) 2014 Jonas Wagner 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | (function(){ 27 | "use strict"; 28 | 29 | function SmartCrop(options){ 30 | this.options = extend({}, SmartCrop.DEFAULTS, options); 31 | } 32 | SmartCrop.DEFAULTS = { 33 | width: 0, 34 | height: 0, 35 | aspect: 0, 36 | cropWidth: 0, 37 | cropHeight: 0, 38 | detailWeight: 0.2, 39 | skinColor: [0.78, 0.57, 0.44], 40 | skinBias: 0.01, 41 | skinBrightnessMin: 0.2, 42 | skinBrightnessMax: 1.0, 43 | skinThreshold: 0.8, 44 | skinWeight: 1.8, 45 | saturationBrightnessMin: 0.05, 46 | saturationBrightnessMax: 0.9, 47 | saturationThreshold: 0.4, 48 | saturationBias: 0.2, 49 | saturationWeight: 0.3, 50 | // step * minscale rounded down to the next power of two should be good 51 | scoreDownSample: 8, 52 | step: 8, 53 | scaleStep: 0.1, 54 | minScale: 0.9, 55 | maxScale: 1.0, 56 | edgeRadius: 0.4, 57 | edgeWeight: -20.0, 58 | outsideImportance: -0.5, 59 | ruleOfThirds: true, 60 | prescale: true, 61 | canvasFactory: null, 62 | debug: false 63 | }; 64 | SmartCrop.crop = function(image, options, callback){ 65 | if(options.aspect){ 66 | options.width = options.aspect; 67 | options.height = 1; 68 | } 69 | 70 | // work around images scaled in css by drawing them onto a canvas 71 | if(image.naturalWidth && (image.naturalWidth != image.width || image.naturalHeight != image.height)){ 72 | var c = new SmartCrop(options).canvas(image.naturalWidth, image.naturalHeight), 73 | cctx = c.getContext('2d'); 74 | c.width = image.naturalWidth; 75 | c.height = image.naturalHeight; 76 | cctx.drawImage(image, 0, 0); 77 | image = c; 78 | } 79 | 80 | var scale = 1, 81 | prescale = 1; 82 | if(options.width && options.height) { 83 | scale = min(image.width/options.width, image.height/options.height); 84 | options.cropWidth = ~~(options.width * scale); 85 | options.cropHeight = ~~(options.height * scale); 86 | // img = 100x100, width = 95x95, scale = 100/95, 1/scale > min 87 | // don't set minscale smaller than 1/scale 88 | // -> don't pick crops that need upscaling 89 | options.minScale = min(options.maxScale || SmartCrop.DEFAULTS.maxScale, max(1/scale, (options.minScale||SmartCrop.DEFAULTS.minScale))); 90 | } 91 | var smartCrop = new SmartCrop(options); 92 | if(options.width && options.height) { 93 | if(options.prescale !== false){ 94 | prescale = 1/scale/options.minScale; 95 | if(prescale < 1) { 96 | var prescaledCanvas = smartCrop.canvas(image.width*prescale, image.height*prescale), 97 | ctx = prescaledCanvas.getContext('2d'); 98 | ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, prescaledCanvas.width, prescaledCanvas.height); 99 | image = prescaledCanvas; 100 | smartCrop.options.cropWidth = ~~(options.cropWidth*prescale); 101 | smartCrop.options.cropHeight = ~~(options.cropHeight*prescale); 102 | } 103 | else { 104 | prescale = 1; 105 | } 106 | } 107 | } 108 | var result = smartCrop.analyse(image); 109 | for(var i = 0, i_len = result.crops.length; i < i_len; i++) { 110 | var crop = result.crops[i]; 111 | crop.x = ~~(crop.x/prescale); 112 | crop.y = ~~(crop.y/prescale); 113 | crop.width = ~~(crop.width/prescale); 114 | crop.height = ~~(crop.height/prescale); 115 | } 116 | callback(result); 117 | return result; 118 | }; 119 | // check if all the dependencies are there 120 | SmartCrop.isAvailable = function(options){ 121 | try { 122 | var s = new this(options), 123 | c = s.canvas(16, 16); 124 | return typeof c.getContext === 'function'; 125 | } 126 | catch(e){ 127 | return false; 128 | } 129 | }; 130 | SmartCrop.prototype = { 131 | canvas: function(w, h){ 132 | if(this.options.canvasFactory !== null){ 133 | return this.options.canvasFactory(w, h); 134 | } 135 | var c = document.createElement('canvas'); 136 | c.width = w; 137 | c.height = h; 138 | return c; 139 | }, 140 | edgeDetect: function(i, o){ 141 | var id = i.data, 142 | od = o.data, 143 | w = i.width, 144 | h = i.height; 145 | for(var y = 0; y < h; y++) { 146 | for(var x = 0; x < w; x++) { 147 | var p = (y*w+x)*4, 148 | lightness; 149 | if(x === 0 || x >= w-1 || y === 0 || y >= h-1){ 150 | lightness = sample(id, p); 151 | } 152 | else { 153 | lightness = sample(id, p)*4 - sample(id, p-w*4) - sample(id, p-4) - sample(id, p+4) - sample(id, p+w*4); 154 | } 155 | od[p+1] = lightness; 156 | } 157 | } 158 | }, 159 | skinDetect: function(i, o){ 160 | var id = i.data, 161 | od = o.data, 162 | w = i.width, 163 | h = i.height, 164 | options = this.options; 165 | for(var y = 0; y < h; y++) { 166 | for(var x = 0; x < w; x++) { 167 | var p = (y*w+x)*4, 168 | lightness = cie(id[p], id[p+1], id[p+2])/255, 169 | skin = this.skinColor(id[p], id[p+1], id[p+2]); 170 | if(skin > options.skinThreshold && lightness >= options.skinBrightnessMin && lightness <= options.skinBrightnessMax){ 171 | od[p] = (skin-options.skinThreshold)*(255/(1-options.skinThreshold)); 172 | } 173 | else { 174 | od[p] = 0; 175 | } 176 | } 177 | } 178 | }, 179 | saturationDetect: function(i, o){ 180 | var id = i.data, 181 | od = o.data, 182 | w = i.width, 183 | h = i.height, 184 | options = this.options; 185 | for(var y = 0; y < h; y++) { 186 | for(var x = 0; x < w; x++) { 187 | var p = (y*w+x)*4, 188 | lightness = cie(id[p], id[p+1], id[p+2])/255, 189 | sat = saturation(id[p], id[p+1], id[p+2]); 190 | if(sat > options.saturationThreshold && lightness >= options.saturationBrightnessMin && lightness <= options.saturationBrightnessMax){ 191 | od[p+2] = (sat-options.saturationThreshold)*(255/(1-options.saturationThreshold)); 192 | } 193 | else { 194 | od[p+2] = 0; 195 | } 196 | } 197 | } 198 | }, 199 | crops: function(image){ 200 | var crops = [], 201 | width = image.width, 202 | height = image.height, 203 | options = this.options, 204 | minDimension = min(width, height), 205 | cropWidth = options.cropWidth || minDimension, 206 | cropHeight = options.cropHeight || minDimension; 207 | for(var scale = options.maxScale; scale >= options.minScale; scale -= options.scaleStep){ 208 | for(var y = 0; y+cropHeight*scale <= height; y+=options.step) { 209 | for(var x = 0; x+cropWidth*scale <= width; x+=options.step) { 210 | crops.push({ 211 | x: x, 212 | y: y, 213 | width: cropWidth*scale, 214 | height: cropHeight*scale 215 | }); 216 | } 217 | } 218 | } 219 | return crops; 220 | }, 221 | score: function(output, crop){ 222 | var score = { 223 | detail: 0, 224 | saturation: 0, 225 | skin: 0, 226 | total: 0 227 | }, 228 | options = this.options, 229 | od = output.data, 230 | downSample = options.scoreDownSample, 231 | invDownSample = 1/downSample, 232 | outputHeightDownSample = output.height*downSample, 233 | outputWidthDownSample = output.width*downSample, 234 | outputWidth = output.width; 235 | for(var y = 0; y < outputHeightDownSample; y+=downSample) { 236 | for(var x = 0; x < outputWidthDownSample; x+=downSample) { 237 | var p = (~~(y*invDownSample)*outputWidth+~~(x*invDownSample))*4, 238 | importance = this.importance(crop, x, y), 239 | detail = od[p+1]/255; 240 | score.skin += od[p]/255*(detail+options.skinBias)*importance; 241 | score.detail += detail*importance; 242 | score.saturation += od[p+2]/255*(detail+options.saturationBias)*importance; 243 | } 244 | 245 | } 246 | score.total = (score.detail*options.detailWeight + score.skin*options.skinWeight + score.saturation*options.saturationWeight)/crop.width/crop.height; 247 | return score; 248 | }, 249 | importance: function(crop, x, y){ 250 | var options = this.options; 251 | 252 | if (crop.x > x || x >= crop.x+crop.width || crop.y > y || y >= crop.y+crop.height) return options.outsideImportance; 253 | x = (x-crop.x)/crop.width; 254 | y = (y-crop.y)/crop.height; 255 | var px = abs(0.5-x)*2, 256 | py = abs(0.5-y)*2, 257 | // distance from edge 258 | dx = Math.max(px-1.0+options.edgeRadius, 0), 259 | dy = Math.max(py-1.0+options.edgeRadius, 0), 260 | d = (dx*dx+dy*dy)*options.edgeWeight; 261 | var s = 1.41-sqrt(px*px+py*py); 262 | if(options.ruleOfThirds){ 263 | s += (Math.max(0, s+d+0.5)*1.2)*(thirds(px)+thirds(py)); 264 | } 265 | return s+d; 266 | }, 267 | skinColor: function(r, g, b){ 268 | var mag = sqrt(r*r+g*g+b*b), 269 | options = this.options, 270 | rd = (r/mag-options.skinColor[0]), 271 | gd = (g/mag-options.skinColor[1]), 272 | bd = (b/mag-options.skinColor[2]), 273 | d = sqrt(rd*rd+gd*gd+bd*bd); 274 | return 1-d; 275 | }, 276 | analyse: function(image){ 277 | var result = {}, 278 | options = this.options, 279 | canvas = this.canvas(image.width, image.height), 280 | ctx = canvas.getContext('2d'); 281 | ctx.drawImage(image, 0, 0); 282 | var input = ctx.getImageData(0, 0, canvas.width, canvas.height), 283 | output = ctx.getImageData(0, 0, canvas.width, canvas.height); 284 | this.edgeDetect(input, output); 285 | this.skinDetect(input, output); 286 | this.saturationDetect(input, output); 287 | 288 | var scoreCanvas = this.canvas(ceil(image.width/options.scoreDownSample), ceil(image.height/options.scoreDownSample)), 289 | scoreCtx = scoreCanvas.getContext('2d'); 290 | 291 | ctx.putImageData(output, 0, 0); 292 | scoreCtx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, scoreCanvas.width, scoreCanvas.height); 293 | 294 | var scoreOutput = scoreCtx.getImageData(0, 0, scoreCanvas.width, scoreCanvas.height); 295 | 296 | var topScore = -Infinity, 297 | topCrop = null, 298 | crops = this.crops(image); 299 | 300 | for(var i = 0, i_len = crops.length; i < i_len; i++) { 301 | var crop = crops[i]; 302 | crop.score = this.score(scoreOutput, crop); 303 | if(crop.score.total > topScore){ 304 | topCrop = crop; 305 | topScore = crop.score.total; 306 | } 307 | 308 | } 309 | 310 | result.crops = crops; 311 | result.topCrop = topCrop; 312 | 313 | if(options.debug && topCrop){ 314 | ctx.fillStyle = 'rgba(255, 0, 0, 0.1)'; 315 | ctx.fillRect(topCrop.x, topCrop.y, topCrop.width, topCrop.height); 316 | for (var y = 0; y < output.height; y++) { 317 | for (var x = 0; x < output.width; x++) { 318 | var p = (y * output.width + x) * 4; 319 | var importance = this.importance(topCrop, x, y); 320 | if (importance > 0) { 321 | output.data[p + 1] += importance * 32; 322 | } 323 | 324 | if (importance < 0) { 325 | output.data[p] += importance * -64; 326 | } 327 | output.data[p + 3] = 255; 328 | } 329 | } 330 | ctx.putImageData(output, 0, 0); 331 | ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)'; 332 | ctx.strokeRect(topCrop.x, topCrop.y, topCrop.width, topCrop.height); 333 | result.debugCanvas = canvas; 334 | } 335 | return result; 336 | } 337 | }; 338 | 339 | // aliases and helpers 340 | var min = Math.min, 341 | max = Math.max, 342 | abs = Math.abs, 343 | ceil = Math.ceil, 344 | sqrt = Math.sqrt; 345 | 346 | function extend(o){ 347 | for(var i = 1, i_len = arguments.length; i < i_len; i++) { 348 | var arg = arguments[i]; 349 | if(arg){ 350 | for(var name in arg){ 351 | o[name] = arg[name]; 352 | } 353 | } 354 | } 355 | return o; 356 | } 357 | 358 | // gets value in the range of [0, 1] where 0 is the center of the pictures 359 | // returns weight of rule of thirds [0, 1] 360 | function thirds(x){ 361 | x = ((x-(1/3)+1.0)%2.0*0.5-0.5)*16; 362 | return Math.max(1.0-x*x, 0.0); 363 | } 364 | 365 | function cie(r, g, b){ 366 | return 0.5126*b + 0.7152*g + 0.0722*r; 367 | } 368 | function sample(id, p) { 369 | return cie(id[p], id[p+1], id[p+2]); 370 | } 371 | function saturation(r, g, b){ 372 | var maximum = max(r/255, g/255, b/255), minumum = min(r/255, g/255, b/255); 373 | if(maximum === minumum){ 374 | return 0; 375 | } 376 | var l = (maximum + minumum) / 2, 377 | d = maximum-minumum; 378 | return l > 0.5 ? d/(2-maximum-minumum) : d/(maximum+minumum); 379 | } 380 | 381 | // amd 382 | if (typeof define !== 'undefined' && define.amd) define(function(){return SmartCrop;}); 383 | //common js 384 | if (typeof exports !== 'undefined') exports.SmartCrop = SmartCrop; 385 | // browser 386 | else if (typeof navigator !== 'undefined') window.SmartCrop = SmartCrop; 387 | // nodejs 388 | if (typeof module !== 'undefined') { 389 | module.exports = SmartCrop; 390 | } 391 | })(); --------------------------------------------------------------------------------