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

Reply via email to