Jonas Kress (WMDE) has uploaded a new change for review. https://gerrit.wikimedia.org/r/264294
Change subject: [WIP] Page image as image header ...................................................................... [WIP] Page image as image header Injects the cropped page image as image header via JS Change-Id: Ic82f59713848902d63738546654a01c47d5ff695 --- M view/resources/jquery/wikibase/jquery.wikibase.itemview.js M view/resources/jquery/wikibase/resources.php M view/resources/wikibase/resources.php A view/resources/wikibase/smartcrop.js A view/resources/wikibase/wikibase.PageImage.js 5 files changed, 519 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Wikibase refs/changes/94/264294/1 diff --git a/view/resources/jquery/wikibase/jquery.wikibase.itemview.js b/view/resources/jquery/wikibase/jquery.wikibase.itemview.js index e937813..8492082 100644 --- a/view/resources/jquery/wikibase/jquery.wikibase.itemview.js +++ b/view/resources/jquery/wikibase/jquery.wikibase.itemview.js @@ -42,6 +42,12 @@ _create: function() { this._createEntityview(); + var pageImage = new wikibase.PageImage(); + + pageImage.getPageImage().done(function( element ){ + $('.wikibase-title').append( element ); + }); + this.$statements = $( '.wikibase-statementgrouplistview', this.element ); if ( this.$statements.length === 0 ) { this.$statements = $( '<div/>' ).appendTo( this.element ); diff --git a/view/resources/jquery/wikibase/resources.php b/view/resources/jquery/wikibase/resources.php index a52543f..d162a03 100644 --- a/view/resources/jquery/wikibase/resources.php +++ b/view/resources/jquery/wikibase/resources.php @@ -227,6 +227,7 @@ 'dependencies' => array( 'jquery.wikibase.entityview', 'jquery.wikibase.sitelinkgrouplistview', + 'wikibase.PageImage' ), ), diff --git a/view/resources/wikibase/resources.php b/view/resources/wikibase/resources.php index 962006c..2af4465 100644 --- a/view/resources/wikibase/resources.php +++ b/view/resources/wikibase/resources.php @@ -57,6 +57,16 @@ ) ), + 'wikibase.PageImage' => $moduleTemplate + array( + 'scripts' => array( + 'wikibase.PageImage.js', + 'smartcrop.js', + + ), + 'dependencies' => array( + 'wikibase' + ) + ), 'wikibase.templates' => $moduleTemplate + array( 'class' => 'Wikibase\View\Module\TemplateModule', 'scripts' => 'templates.js', diff --git a/view/resources/wikibase/smartcrop.js b/view/resources/wikibase/smartcrop.js new file mode 100644 index 0000000..250e2de --- /dev/null +++ b/view/resources/wikibase/smartcrop.js @@ -0,0 +1,391 @@ +/** 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), minumum = min(r/255, g/255, b/255); + if(maximum === minumum){ + return 0; + } + var l = (maximum + minumum) / 2, + d = maximum-minumum; + return l > 0.5 ? d/(2-maximum-minumum) : d/(maximum+minumum); +} + +// 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; +} +})(); diff --git a/view/resources/wikibase/wikibase.PageImage.js b/view/resources/wikibase/wikibase.PageImage.js new file mode 100644 index 0000000..77beda3 --- /dev/null +++ b/view/resources/wikibase/wikibase.PageImage.js @@ -0,0 +1,111 @@ +/** + * @licence GNU GPL v2+ + * @author Jonas Kress + */ + +(function(wb, mw, $) { + 'use strict'; + + /** + * Offers access to the page image + * @constructor + * + * @param {int} width + * @param {int} height + */ + var SELF = wb.PageImage = function PageImage(width, height) { + if (width) { + this._width = width; + } + if (height) { + this._height = height; + } + }; + + SELF.prototype._width = 200; + SELF.prototype._height = 200; + + /** + * 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(); + + var self = this; + + self._getImageUrl().done( function( url ){ + self._loadImage( url ).done( + function(image) { + self._getSmartCrop(image).done(function(crop) { + deferred.resolve(self._getMaskedImage(image, crop)); + }); + }); + } ); + + return deferred.promise(); + }; + + SELF.prototype._loadImage = function(url) { + var deferred = $.Deferred(); + + var image = new Image(); + image.onload = function() { + deferred.resolve(image); + }; + image.crossOrigin = 'https://upload.wikimedia.org/crossdomain.xml'; + image.src = url; + + return deferred.promise(); + } + + SELF.prototype._getSmartCrop = function(image) { + var deferred = $.Deferred(); + + SmartCrop.crop(image, { + width : this._width, + height : this._height, + minScale : 1, + }, function(result) { + deferred.resolve(result.topCrop); + }); + + return deferred.promise(); + }; + + SELF.prototype._getMaskedImage = function(image, crop) { + + var canvas = $('<canvas/>')[0], ctx = canvas.getContext('2d'); + + 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 canvas; + }; + + SELF.prototype._getImageUrl = function() { + var deferred = $.Deferred(); + + mw.loader.using('mediawiki.api', function() { + (new mw.Api()).get({ + action : 'query', + prop : 'pageimages', + piprop: 'thumbnail', + pithumbsize: '900', + titles: mw.config.get('wgPageName') + }).done(function(data) { + var thumb = data.query.pages[Object.keys(data.query.pages)[0]].thumbnail; + console.log(data); + if( thumb ){ + deferred.resolve(thumb.source); + } + }); + }); + + return deferred.promise(); + }; + +}(wikibase, mw, jQuery)); \ No newline at end of file -- To view, visit https://gerrit.wikimedia.org/r/264294 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ic82f59713848902d63738546654a01c47d5ff695 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Wikibase Gerrit-Branch: master Gerrit-Owner: Jonas Kress (WMDE) <jonas.kr...@wikimedia.de> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits