MediaWiki:Gadget-ImageHeader.js

/** smart-crop.js * A javascript library implementing content aware image cropping * * Copyright (C) 2014 Jonas Wagner * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function{ "use strict";

function SmartCrop(options){ this.options = extend({}, SmartCrop.DEFAULTS, options); } SmartCrop.DEFAULTS = { width: 0, height: 0, aspect: 0, cropWidth: 0, cropHeight: 0, detailWeight: 0.2, skinColor: [0.78, 0.57, 0.44], skinBias: 0.01, skinBrightnessMin: 0.2, skinBrightnessMax: 1.0, skinThreshold: 0.8, skinWeight: 1.8, saturationBrightnessMin: 0.05, saturationBrightnessMax: 0.9, saturationThreshold: 0.4, saturationBias: 0.2, saturationWeight: 0.3, // step * minscale rounded down to the next power of two should be good scoreDownSample: 8, step: 8, scaleStep: 0.1, minScale: 0.9, maxScale: 1.0, edgeRadius: 0.4, edgeWeight: -20.0, outsideImportance: -0.5, ruleOfThirds: true, prescale: true, canvasFactory: null, debug: false }; SmartCrop.crop = function(image, options, callback){ if(options.aspect){ options.width = options.aspect; options.height = 1; }

// work around images scaled in css by drawing them onto a canvas if(image.naturalWidth && (image.naturalWidth != image.width || image.naturalHeight != image.height)){ var c = new SmartCrop(options).canvas(image.naturalWidth, image.naturalHeight), cctx = c.getContext('2d'); c.width = image.naturalWidth; c.height = image.naturalHeight; cctx.drawImage(image, 0, 0); image = c;	}

var scale = 1, prescale = 1; if(options.width && options.height) { scale = min(image.width/options.width, image.height/options.height); options.cropWidth = (options.width * scale); options.cropHeight = (options.height * scale); // img = 100x100, width = 95x95, scale = 100/95, 1/scale > min // don't set minscale smaller than 1/scale // -> don't pick crops that need upscaling options.minScale = min(options.maxScale || SmartCrop.DEFAULTS.maxScale, max(1/scale, (options.minScale||SmartCrop.DEFAULTS.minScale))); }	var smartCrop = new SmartCrop(options); if(options.width && options.height) { if(options.prescale !== false){ prescale = 1/scale/options.minScale; if(prescale < 1) { var prescaledCanvas = smartCrop.canvas(image.width*prescale, image.height*prescale), ctx = prescaledCanvas.getContext('2d'); ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, prescaledCanvas.width, prescaledCanvas.height); image = prescaledCanvas; smartCrop.options.cropWidth = (options.cropWidth*prescale); smartCrop.options.cropHeight = (options.cropHeight*prescale); }			else { prescale = 1; }		}	}	var result = smartCrop.analyse(image); for(var i = 0, i_len = result.crops.length; i < i_len; i++) { var crop = result.crops[i]; crop.x = (crop.x/prescale); crop.y = (crop.y/prescale); crop.width = (crop.width/prescale); crop.height = (crop.height/prescale); }	callback(result); return result; }; // check if all the dependencies are there SmartCrop.isAvailable = function(options){ try { var s = new this(options), c = s.canvas(16, 16); return typeof c.getContext === 'function'; }	catch(e){ return false; } }; SmartCrop.prototype = { canvas: function(w, h){ if(this.options.canvasFactory !== null){ return this.options.canvasFactory(w, h); }		var c = document.createElement('canvas'); c.width = w;		c.height = h;		return c;	}, edgeDetect: function(i, o){ var id = i.data, od = o.data, w = i.width, h = i.height; for(var y = 0; y < h; y++) { for(var x = 0; x < w; x++) { var p = (y*w+x)*4, lightness; if(x === 0 || x >= w-1 || y === 0 || y >= h-1){ lightness = sample(id, p); }				else { lightness = sample(id, p)*4 - sample(id, p-w*4) - sample(id, p-4) - sample(id, p+4) - sample(id, p+w*4); }				od[p+1] = lightness; }		}	},	skinDetect: function(i, o){ var id = i.data, od = o.data, w = i.width, h = i.height, options = this.options; for(var y = 0; y < h; y++) { for(var x = 0; x < w; x++) { var p = (y*w+x)*4, lightness = cie(id[p], id[p+1], id[p+2])/255, skin = this.skinColor(id[p], id[p+1], id[p+2]); if(skin > options.skinThreshold && lightness >= options.skinBrightnessMin && lightness <= options.skinBrightnessMax){ od[p] = (skin-options.skinThreshold)*(255/(1-options.skinThreshold)); }				else { od[p] = 0; }			}		}	},	saturationDetect: function(i, o){ var id = i.data, od = o.data, w = i.width, h = i.height, options = this.options; for(var y = 0; y < h; y++) { for(var x = 0; x < w; x++) { var p = (y*w+x)*4, lightness = cie(id[p], id[p+1], id[p+2])/255, sat = saturation(id[p], id[p+1], id[p+2]); if(sat > options.saturationThreshold && lightness >= options.saturationBrightnessMin && lightness <= options.saturationBrightnessMax){ od[p+2] = (sat-options.saturationThreshold)*(255/(1-options.saturationThreshold)); }				else { od[p+2] = 0; }			}		}	},	crops: function(image){ var crops = [], width = image.width, height = image.height, options = this.options, minDimension = min(width, height), cropWidth = options.cropWidth || minDimension, cropHeight = options.cropHeight || minDimension; for(var scale = options.maxScale; scale >= options.minScale; scale -= options.scaleStep){ for(var y = 0; y+cropHeight*scale <= height; y+=options.step) { for(var x = 0; x+cropWidth*scale <= width; x+=options.step) { crops.push({						x: x,						y: y,						width: cropWidth*scale,						height: cropHeight*scale					}); }			}		}		return crops; },	score: function(output, crop){ var score = { detail: 0, saturation: 0, skin: 0, total: 0 },			options = this.options, od = output.data, downSample = options.scoreDownSample, invDownSample = 1/downSample, outputHeightDownSample = output.height*downSample, outputWidthDownSample = output.width*downSample, outputWidth = output.width; for(var y = 0; y < outputHeightDownSample; y+=downSample) { for(var x = 0; x < outputWidthDownSample; x+=downSample) { var p = ((y*invDownSample)*outputWidth+(x*invDownSample))*4, importance = this.importance(crop, x, y), detail = od[p+1]/255; score.skin += od[p]/255*(detail+options.skinBias)*importance; score.detail += detail*importance; score.saturation += od[p+2]/255*(detail+options.saturationBias)*importance; }

}		score.total = (score.detail*options.detailWeight + score.skin*options.skinWeight + score.saturation*options.saturationWeight)/crop.width/crop.height; return score; },	importance: function(crop, x, y){ var options = this.options;

if (crop.x > x || x >= crop.x+crop.width || crop.y > y || y >= crop.y+crop.height) return options.outsideImportance; x = (x-crop.x)/crop.width; y = (y-crop.y)/crop.height; var px = abs(0.5-x)*2, py = abs(0.5-y)*2, // distance from edge dx = Math.max(px-1.0+options.edgeRadius, 0), dy = Math.max(py-1.0+options.edgeRadius, 0), d = (dx*dx+dy*dy)*options.edgeWeight; var s = 1.41-sqrt(px*px+py*py); if(options.ruleOfThirds){ s += (Math.max(0, s+d+0.5)*1.2)*(thirds(px)+thirds(py)); }		return s+d; },	skinColor: function(r, g, b){ var mag = sqrt(r*r+g*g+b*b), options = this.options, rd = (r/mag-options.skinColor[0]), gd = (g/mag-options.skinColor[1]), bd = (b/mag-options.skinColor[2]), d = sqrt(rd*rd+gd*gd+bd*bd); return 1-d; },	analyse: function(image){ var result = {}, options = this.options, canvas = this.canvas(image.width, image.height), ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0); var input = ctx.getImageData(0, 0, canvas.width, canvas.height), output = ctx.getImageData(0, 0, canvas.width, canvas.height); this.edgeDetect(input, output); this.skinDetect(input, output); this.saturationDetect(input, output);

var scoreCanvas = this.canvas(ceil(image.width/options.scoreDownSample), ceil(image.height/options.scoreDownSample)), scoreCtx = scoreCanvas.getContext('2d');

ctx.putImageData(output, 0, 0); scoreCtx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, scoreCanvas.width, scoreCanvas.height);

var scoreOutput = scoreCtx.getImageData(0, 0, scoreCanvas.width, scoreCanvas.height);

var topScore = -Infinity, topCrop = null, crops = this.crops(image);

for(var i = 0, i_len = crops.length; i < i_len; i++) { var crop = crops[i]; crop.score = this.score(scoreOutput, crop); if(crop.score.total > topScore){ topCrop = crop; topScore = crop.score.total; }

}

result.crops = crops; result.topCrop = topCrop;

if(options.debug && topCrop){ ctx.fillStyle = 'rgba(255, 0, 0, 0.1)'; ctx.fillRect(topCrop.x, topCrop.y, topCrop.width, topCrop.height); for (var y = 0; y < output.height; y++) { for (var x = 0; x < output.width; x++) { var p = (y * output.width + x) * 4; var importance = this.importance(topCrop, x, y); if (importance > 0) { output.data[p + 1] += importance * 32; }

if (importance < 0) { output.data[p] += importance * -64; }					output.data[p + 3] = 255; }			}			ctx.putImageData(output, 0, 0); ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)'; ctx.strokeRect(topCrop.x, topCrop.y, topCrop.width, topCrop.height); result.debugCanvas = canvas; }		return result; } };

// aliases and helpers var min = Math.min, max = Math.max, abs = Math.abs, ceil = Math.ceil, sqrt = Math.sqrt;

function extend(o){ for(var i = 1, i_len = arguments.length; i < i_len; i++) { var arg = arguments[i]; if(arg){ for(var name in arg){ o[name] = arg[name]; }		}	}	return o; }

// gets value in the range of [0, 1] where 0 is the center of the pictures // returns weight of rule of thirds [0, 1] function thirds(x){ x = ((x-(1/3)+1.0)%2.0*0.5-0.5)*16; return Math.max(1.0-x*x, 0.0); }

function cie(r, g, b){ return 0.5126*b + 0.7152*g + 0.0722*r; } function sample(id, p) { return cie(id[p], id[p+1], id[p+2]); } function saturation(r, g, b){ var maximum = max(r/255, g/255, b/255), minimum = min(r/255, g/255, b/255); if(maximum === minimum){ return 0; }	var l = (maximum + minimum) / 2, d = maximum-minimum; return l > 0.5 ? d/(2-maximum-minimum) : d/(maximum+minimum); }

// amd if (typeof define !== 'undefined' && define.amd) define(function{return SmartCrop;}); //common js if (typeof exports !== 'undefined') exports.SmartCrop = SmartCrop; // browser else if (typeof navigator !== 'undefined') window.SmartCrop = SmartCrop; // nodejs if (typeof module !== 'undefined') { module.exports = SmartCrop; } });

var mw = mw || {}; var wikibase = wikibase || {};

/** * @licence GNU GPL v2+ * @author Jonas Kress */

( function( wb, mw, $, SmartCrop ) {	'use strict';

/**	 * Offers access to the page image *	 * Uses pageimages and pageprops Api endpoints to retrieve the image URL *	 * @constructor *	 * @param {int} (optional) width * @param {int} (optional) height * @param {int} (optional) thumbnailSize * 		 Size of the thumbnail image which is loaded and cropped */	var SELF = wb.PageImage = function PageImage( width, height, thumbnailSize ) {

if ( width ) { this._width = Math.round( width ); }		if ( height ) { this._height = Math.round( height ); }

if ( thumbnailSize ) { this._thumbnailSize = Math.round( thumbnailSize ); } else { this._thumbnailSize = 2 * Math.max( this._height, this._width ); }	};

SELF.prototype._width = 200; SELF.prototype._height = 200;

SELF.prototype._thumbnailSize = 400; SELF.prototype._minScale = 1; SELF.prototype._crossOriginXmlUrl = 'https://upload.wikimedia.org/crossdomain.xml';

/**	 * Returns the page image as DOM element * @return {Object} jQuery.Promise Resolved after loading and cropping of image is done *		 returning a DOM element. */	SELF.prototype.getPageImage = function { var deferred = $.Deferred, self = this;

self._getImageUrl.done( function( url, referenceUrl ) {			self._loadImage( url ).done( function( image ) { self._getSmartCrop( image ).done( function( crop ) {						deferred.resolve( self._getMaskedImage( image, crop, referenceUrl ) );					} ); } );		} );

return deferred.promise; };

/**	 * @private **/	SELF.prototype._loadImage = function( url ) { var deferred = $.Deferred;

var image = new Image; image.onload = function { deferred.resolve( image ); };		image.crossOrigin = this._crossOriginXmlUrl; image.src = url;

return deferred.promise; };

/**	 * @private **/	SELF.prototype._getSmartCrop = function( image ) { var deferred = $.Deferred;

SmartCrop.crop( image, {			width : this._width,			height : this._height,			minScale: this._minScale,		}, function( result ) {			deferred.resolve( result.topCrop );		} );

return deferred.promise; };

/**	 * @private **/	SELF.prototype._getMaskedImage = function( image, crop, ref ) {

var canvas = $( ' ' )[0], ctx = canvas.getContext( '2d' );

if ( ref ) { $( canvas ).data( 'ref', ref ); }

canvas.width = this._width; canvas.height = this._height; ctx.drawImage( image, crop.x, crop.y, crop.width, crop.height, 0, 0,				canvas.width, canvas.height );

return $( '' ).attr( 'href', 'https://commons.wikimedia.org/wiki/File:' + ref ).append( canvas ); };

/**	 * @private **/	SELF.prototype._getImageUrl = function { var deferred = $.Deferred, self = this;

mw.loader.using( 'mediawiki.api', function {			( new mw.Api ).get( { action : 'query', prop : 'pageimages|pageprops', piprop: 'thumbnail', pilicense: 'any', pithumbsize: self._thumbnailSize, titles: mw.config.get( 'wgPageName' ) } ).done( function( data ) { var page = data.query.pages[Object.keys( data.query.pages )[0]]; if( page.thumbnail ){ deferred.resolve( page.thumbnail.source,						page.pageprops.page_image_free || page.pageprops.page_image );// jshint ignore:line }			} );		});

return deferred.promise; };

if ( mw.config.get( 'wgNamespaceNumber' ) !== 0 || mw.config.get( 'wgAction' ) !== 'view' || mw.config.get( 'wbIsEditView' ) !== true ) { return; }

var pageImage = new wikibase.PageImage( 365, 365 * 1.1 ); var div = $( ' ' ); var position = $( '.wikibase-entityview-side' ).position; if ( mw.config.get( 'wgNamespaceNumber' ) === 0 && position.left !== 0 ) { $( '.wikibase-entityview-side' ).prepend( div ); pageImage.getPageImage.done( function( element ) {			div.append( element ) ;		} ); }

}( wikibase, mediaWiki, jQuery, window.SmartCrop ) );