MediaWiki:Gadget-ImageHeader.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/** 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 = $( '<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 $( '<a>' ).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 = $( '<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 ) );