├── 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 | })();
--------------------------------------------------------------------------------