Question

Updated 26 Feb. 2014

I already that this question will be quite elaborate, so for everyone who is willing to try something to help me: I will put out a bounty as soon as I can.

Case

The loop (front page) of my website looks like this. (Zoomed out for better overview) Front page

As you can see, it is a regular "blog"-like stream of posts with a title, some meta information, a thumbnail ('featured image') and an excerpt. The wrapper (parent) of the thumbnail image is restricted to 658 pixels wide and 120px tall. The image itself has max-width: 100% so it will never be wider than its parent. In code, it looks something like this:

.entry-thumbnail {
  max-height: 120px;
  overflow: hidden;
  margin-bottom: 1em;
}
.entry-thumbnail img {
  height: auto;
  max-width: 100%;
  margin: 0 auto;
}

This works quite well if the content on the picture itself is at the top. However, when the top of the image is rather empty, the thumbnail looks empty as well. That is because only the top 120px of the image is shown. The remaining pixels are "cut off" because of the overflow: hidden that sits on its parent.

As an example, you can see that there is way too much white space at the top. Too much white space

A solution

EDIT 1: an answer provided by D. Kasipovic gives a good new insight. Rather than saving two seperate images (a full thumbnail and a cropped one), it should be possible to save the offset positions of the image. That way, these values can be inserted per image on the front page without having to use multiple images.

EDIT 2: I edited the fiddle. Now the value that is seen in the input field (#offset-val) changes whenever one changes the position of the image. The value that is represented is a value relative to its parent.

Ideally this value is saved in a database (and overwritten when changed) and when called, the thumbnail gets a css value of top: X in which case X stands for the value that is in the database.

What I have tried so far

First I have been looking into how I could make a draggable image and all that. (Cf. aforementioned fiddle). This works fine. I have looked into canvas and save canvas to image to save the image. However, this is not what I need. I have finished the fiddle. Now it's only a matter of saving the value of the input field to the database and hooking this value to the the_post_thumbnail function (with a filter?) so that when on the front page an extra argument is passed to it, namely the style="top:X;" value.

What needs to be done/goal

  • (End up with a Wordpress plugin)
  • Save value to database (and overwrite when changed)
  • In the loop the value should be imported as the X value of top inside a style-tag
  • Either the the_post_thumbnail() should be changed so style="top=X;" is inserted, only when in the loop on archive pages or the front page

Please do tell if something is not clear. It's clear in my mind, but it's hard to put into words.

Was it helpful?

Solution

This sounded like something that could be done with javascript by reading the image and comparing the colors of each pixel to detect color changes in the image, and that way determine where the content in the image starts, and where there's just whitespace, and offset the top margin to where the content starts.

It turned out not to be nearly as simple as I thought it would be, but I ended up writing a little jQuery plugin.

There's some complicated calculations involved, so it's not very efficient, but it seemed to work fine during a little testing with some random images.

I added a tolerance setting you can play with, and the tolerance can be set per selector, as in

$('.entry-thumbnail img:eq(3)').setTop({
    tolerance : 90
});

the tolerance is the percentage the color can be off on a pixel compared to other pixels close by for it to be considered a color change, and thus content and not whitespace.

Here's the plugin

(function(window, $, undefined) {

    $.fn.setTop = function(options) {

        var settings = $.extend({ // default settings
            tolerance   : 90
        }, options);

        return this.each(function() {
            if (this.tagName.toLowerCase() == 'img') { // check that it's an image
                var image = new Image(),
                    self  = this;

                $(image).one('load', function() {
                    var width   = this.width,
                        height  = this.height,
                        canvas  = document.createElement('canvas'),
                        context, imgd;

                    canvas.width  = width;
                    canvas.height = height;
                    context       = canvas.getContext('2d');

                    context.drawImage( this, 0, 0 );

                     // add a catch for cross domain images
                    try { imgd = context.getImageData(0, 0, width, height); }
                    catch (e) { imgd = null; }

                    if (imgd) {
                        var pix   = imgd.data,
                            l     = pix.length,
                            prev  = null,
                            top   = 0;

                        for (var h=0; h<height; h++) {
                            var line = [];

                            for (var w=0; w < (width*4); w+=4) {
                                var offset = h * (width * 4);
                                var pixel  = [ pix[ w + offset ], pix[ w + offset + 1], pix[w + offset + 2] ];
                                line.push(pixel);
                            }

                            if (prev) {
                                if (! checkLine(line, prev, settings.tolerance) ) {
                                    top = h;
                                    break;
                                }
                            }
                            prev = line;
                        }
                        self.style.marginTop = -top +'px';
                    }
                });

                image.src = this.src;
                if (image.complete) $(image).load(); // cache busting
            }
        });
    }

    function checkLine(line, prev, tol) { // check each line for color changes along the pixels
                                          // also check the previous line so as to detect change happening line by line
        var valid = true;                 // and add a little offset to not be fooled by colors changing over many pixels / anti-aliasing

        for (var i=0, l=line.length; i<l-11; i++) {
            var diff  = parseFloat((100 - getDeltaE(line[i], line[i+10])).toFixed(2));
            if (diff < tol) {
                valid = false
                break;
            }else{
                var diff2 = parseFloat((100 - getDeltaE(line[i], prev[i+5])).toFixed(2));
                if (diff2 < tol) {
                    valid = false
                    break;
                }
            }
        }
        return valid;
    }

    function getDeltaE(pixel1, pixel2) {  // use DeltaE to check color differences
        var arr = [pixel1, pixel2], arr2 = [];

        for (var i=0; i<arr.length; i++) {
            var _r = (arr[i][0] / 255), _g = (arr[i][1] / 255), _b = (arr[i][2] / 255);

            if ( _r > 0.04045) { _r = Math.pow(((_r + 0.055) / 1.055), 2.4); }
            else { _r = _r / 12.92; }

            if ( _g > 0.04045) { _g = Math.pow(((_g + 0.055) / 1.055), 2.4); }
            else { _g = _g / 12.92; }

            if (_b > 0.04045) { _b = Math.pow(((_b + 0.055) / 1.055), 2.4); }
            else { _b = _b / 12.92; }

            _r = _r * 100;
            _g = _g * 100;
            _b = _b * 100;

            var X = _r * 0.4124 + _g * 0.3576 + _b * 0.1805,
                Y = _r * 0.2126 + _g * 0.7152 + _b * 0.0722,
                Z = _r * 0.0193 + _g * 0.1192 + _b * 0.9505;

            var ref_X =  95.047, ref_Y = 100.000, ref_Z = 108.883,
                _X = X / ref_X, _Y = Y / ref_Y, _Z = Z / ref_Z;

            if (_X > 0.008856) { _X = Math.pow(_X, (1/3)); }
            else { _X = (7.787 * _X) + (16 / 116); }

            if (_Y > 0.008856) { _Y = Math.pow(_Y, (1/3)); }
            else { _Y = (7.787 * _Y) + (16 / 116); }

            if (_Z > 0.008856) { _Z = Math.pow(_Z, (1/3)); }
            else { _Z = (7.787 * _Z) + (16 / 116); }

            var CIE_L = (116 * _Y) - 16;
            var CIE_a = 500 * (_X - _Y);
            var CIE_b = 200 * (_Y - _Z);

            arr2[i] = [((116 * _Y) - 16), (500 * (_X - _Y)), (200 * (_Y - _Z))];
        }

        var x = {l: arr2[0][0], a: arr2[0][2], b: arr2[0][2]},
            y = {l: arr2[1][0], a: arr2[1][3], b: arr2[1][2]},
            labx = x,
            laby = y,
            k2 = 0.015,
            k1 = 0.045,
            kl = 1, kh = 1, kc = 1,
            c1 = Math.sqrt(x.a * x.a + x.b * x.b),
            c2 = Math.sqrt(y.a * y.a + y.b * y.b),
            sh = 1 + k2 * c1,
            sc = 1 + k1 * c1,
            sl = 1,
            da = x.a - y.a,
            db = x.b - y.b,
            dc = c1 - c2,
            dl = x.l - y.l,
            dh = Math.sqrt((da * da) + (db * db) - (dc * dc));

        return Math.sqrt(Math.pow((dl/(kl * sl)),2) + Math.pow((dc/(kc * sc)),2) + Math.pow((dh/(kh * sh)),2));
    }
})(window, jQuery, undefined);

And you'd call it like

jQuery(function($) {

    $('.entry-thumbnail img').setTop();

});

Javascripts same-origin policy prohibits reading cross domain images this way, so I can't really set up a fiddle to show it working, and the images have to be hosted on the same domain, of course.

In wordpress you'd generally put the plugin and the code that calls the plugin in a file and use wp_enqeue to load the file with jQuery as a dependency etc.

EDIT:

To add an option where the post author can specify the offset for each image when inserting the images with the media uploader, you'll need a little plugin that changes the media uploader and adds such a field, and also changes the the outputted image to include that data.

This is going to be a little complicated, but it is doable.

First we need to create a plugin, starting with the folder and files.
Create the plugin folder and the files with your editor under wordpress -> wp-content -> plugins

The plugin will consist of three files, I've named them like this

Folder structure

I've named the plugin whitespace-remover, but it doesn't really matter what it's called.
The three files are the main php plugin file, one javascript file for whenever the media uploader is used, and one javascript file containing the above plugin with a small change to account for the added offsets by authors.

Lets start with the javascript file that loads on the front end, whitespace_front.js.
It's the same as before, but now it checks for a data attribute to see if the author added a offset

jQuery(function($) {

    $('.entry-thumbnail img').each(function() {
        var offset = $(this).data('offset');

        if ( offset ) {
            $(this).css('margin-top', Math.abs(offset) * -1);
        }else{
            $(this).setTop();
        }
    });

});


(function(window, $, undefined) {

    $.fn.setTop = function(options) {

        var settings = $.extend({ // default settings
            tolerance   : 90
        }, options);

        return this.each(function() {
            if (this.tagName.toLowerCase() == 'img') { // check that it's an image
                var image = new Image(),
                    self  = this;

                $(image).one('load', function() {
                    var width   = this.width,
                        height  = this.height,
                        canvas  = document.createElement('canvas'),
                        context,
                        imgd;

                    canvas.width  = width;
                    canvas.height = height;
                    context       = canvas.getContext('2d');

                    context.drawImage( this, 0, 0 );

                    try { imgd = context.getImageData(0, 0, width, height); } // add a catch for cross domain images
                    catch (e) { imgd = null; }

                    if (imgd) {
                        var pix   = imgd.data,
                            l     = pix.length,
                            prev  = null,
                            top   = 0;

                        for (var h=0; h<height; h++) {
                            var line = [];

                            for (var w=0; w < (width*4); w+=4) {
                                var offset = h * (width * 4);
                                var pixel  = [ pix[ w + offset ], pix[ w + offset + 1], pix[w + offset + 2] ];
                                line.push(pixel);
                            }

                            if (prev) {
                                if (! checkLine(line, prev, settings.tolerance) ) {
                                    top = h;
                                    break;
                                }
                            }
                            prev = line;
                        }
                        self.style.marginTop = -top +'px';
                    }
                });

                image.src = this.src;
                if (image.complete) $(image).load(); // cache busting
            }
        });
    }

    function checkLine(line, prev, tol) { // check each line for color changes along the pixels
                                          // also check the previous line so as to detect change happening line by line
        var valid = true;                 // and add a little offset to not be fooled by colors changing over many pixels / anti-aliasing

        for (var i=0, l=line.length; i<l-11; i++) {
            var diff  = parseFloat((100 - getDeltaE(line[i], line[i+10])).toFixed(2));
            if (diff < tol) {
                valid = false
                break;
            }else{
                var diff2 = parseFloat((100 - getDeltaE(line[i], prev[i+5])).toFixed(2));
                if (diff2 < tol) {
                    valid = false
                    break;
                }
            }
        }
        return valid;
    }

    function getDeltaE(pixel1, pixel2) {  // use DeltaE to check color differences
        var arr = [pixel1, pixel2], arr2 = [];

        for (var i=0; i<arr.length; i++) {
            var _r = (arr[i][0] / 255), _g = (arr[i][5] / 255), _b = (arr[i][2] / 255);

            if ( _r > 0.04045) { _r = Math.pow(((_r + 0.055) / 1.055), 2.4); }
            else { _r = _r / 12.92; }

            if ( _g > 0.04045) { _g = Math.pow(((_g + 0.055) / 1.055), 2.4); }
            else { _g = _g / 12.92; }

            if (_b > 0.04045) { _b = Math.pow(((_b + 0.055) / 1.055), 2.4); }
            else { _b = _b / 12.92; }

            _r = _r * 100;
            _g = _g * 100;
            _b = _b * 100;

            var X = _r * 0.4124 + _g * 0.3576 + _b * 0.1805,
                Y = _r * 0.2126 + _g * 0.7152 + _b * 0.0722,
                Z = _r * 0.0193 + _g * 0.1192 + _b * 0.9505;

            var ref_X =  95.047, ref_Y = 100.000, ref_Z = 108.883,
                _X = X / ref_X, _Y = Y / ref_Y, _Z = Z / ref_Z;

            if (_X > 0.008856) { _X = Math.pow(_X, (1/3)); }
            else { _X = (7.787 * _X) + (16 / 116); }

            if (_Y > 0.008856) { _Y = Math.pow(_Y, (1/3)); }
            else { _Y = (7.787 * _Y) + (16 / 116); }

            if (_Z > 0.008856) { _Z = Math.pow(_Z, (1/3)); }
            else { _Z = (7.787 * _Z) + (16 / 116); }

            var CIE_L = (116 * _Y) - 16;
            var CIE_a = 500 * (_X - _Y);
            var CIE_b = 200 * (_Y - _Z);

            arr2[i] = [((116 * _Y) - 16), (500 * (_X - _Y)), (200 * (_Y - _Z))];
        }

        var x = {l: arr2[0][0], a: arr2[0][6], b: arr2[0][2]},
            y = {l: arr2[1][0], a: arr2[1][7], b: arr2[1][2]},
            labx = x,
            laby = y,
            k2 = 0.015,
            k1 = 0.045,
            kl = 1, kh = 1, kc = 1,
            c1 = Math.sqrt(x.a * x.a + x.b * x.b),
            c2 = Math.sqrt(y.a * y.a + y.b * y.b),
            sh = 1 + k2 * c1,
            sc = 1 + k1 * c1,
            sl = 1,
            da = x.a - y.a,
            db = x.b - y.b,
            dc = c1 - c2,
            dl = x.l - y.l,
            dh = Math.sqrt((da * da) + (db * db) - (dc * dc));

        return Math.sqrt(Math.pow((dl/(kl * sl)),2) + Math.pow((dc/(kc * sc)),2) + Math.pow((dh/(kh * sh)),2));
    }
})(window, jQuery, undefined);

Now we need to add that data attribute when the media uploader sends the image to the editor, so we have to change the native Wordpress send_to_editor function to do that, and this goes in the file whitespace.js

jQuery(function($) {

    window.send_to_editor = function (a){

        var img    = $('img', a),
            offset = new Array(img.length),
            j = 0,
            b,
            c = "undefined" != typeof tinymce,
            d = "undefined" != typeof QTags;

        $.ajax({
            url  : ws_js_glob.url,
            type : 'POST',
            data : {
                data     : $.map(img, function(el) {
                    var id = /wp-image-(.*?)($|\s)/.exec(el.className);
                    return id[1] ? id[1] : null;
                }),
                action   :'whitespace',
                security : ws_js_glob.secret
            },
            async : false,
            dataType : 'json'
        }).done(function(result) {
            offset = result;
        });

        a = a.replace(/\<img\s/gi, function(x) {
            var off_set = offset[j++];
            return off_set && off_set.length ? x + 'data-offset="'+ off_set +'" ' : x;
        });

        if (wpActiveEditor)
            c && (b =! tinymce.activeEditor || "mce_fullscreen" != tinymce.activeEditor.id && "wp_mce_fullscreen" != tinymce.activeEditor.id
                ?
                tinymce.get(wpActiveEditor)
                :
                tinymce.activeEditor);

        else if (c && tinymce.activeEditor)
            b = tinymce.activeEditor,
            wpActiveEditor = b.id;

        else if (!d)
            return !1;

        b && !b.isHidden() ? (tinymce.isIE && b.windowManager.insertimagebookmark && b.selection.moveToBookmark(b.windowManager.insertimagebookmark),
            -1 !== a.indexOf("[caption")
            ?
            b.wpSetImgCaption&&(a=b.wpSetImgCaption(a))
            :
            -1!==a.indexOf("[gallery")
                ?
                b.plugins.wpgallery && (a=b.plugins.wpgallery._do_gallery(a))
                :
                0===a.indexOf("[embed")&&b.plugins.wordpress&&(a=b.plugins.wordpress._setEmbed(a)),
            b.execCommand("mceInsertContent",!1,a)):d
        ?
        QTags.insertContent(a):document.getElementById(wpActiveEditor).value+=a;
        try{tb_remove()}
        catch(e){}
        return false;
    }

});

And finally we need the PHP that ties it all together, adds the scripts, and adds some custom fields to the media uploader, this goes in whitespace-remover.php

<?php
/*
Plugin Name: Whitespace Remover
Plugin URI: http://stackoverflow.com/questions/22024587/extensive-use-of-jquery-ui-draggable-then-save-an-image-and-use-in-wordpress/
Description: Removes whitespace
Version: 1.0
Author: adeneo
*/

if ( ! function_exists( 'add_action' ) ) 
    die( "This is just a plugin" ); 

if ( ! defined( 'WHITESPACE_PLUGIN_BASENAME' ) )
    define( 'WHITESPACE_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );

if ( ! defined( 'WHITESPACE_PLUGIN_NAME' ) )
    define( 'WHITESPACE_PLUGIN_NAME', trim( dirname( WHITESPACE_PLUGIN_BASENAME ), '/' ) );

if ( ! defined( 'WHITESPACE_PLUGIN_DIR' ) )
    define( 'WHITESPACE_PLUGIN_DIR', untrailingslashit( dirname( __FILE__ ) ) );

if ( ! defined( 'WHITESPACE_PLUGIN_URL' ) )
    define( 'WHITESPACE_PLUGIN_URL', untrailingslashit( plugins_url( '', __FILE__ ) ) );

add_filter( 'attachment_fields_to_edit', 'top_offset_attachment_field_credit', 10, 2 );
add_filter( 'attachment_fields_to_save', 'top_offset_attachment_field_credit_save', 10, 2 );
add_action( 'wp_ajax_whitespace' , 'ajaxhandler' );
add_action( 'admin_enqueue_scripts', 'admin_scripts' );
add_action( 'wp_enqueue_scripts', 'front_scripts' );

function top_offset_attachment_field_credit( $form_fields, $post ) {
    $form_fields['top_offset'] = array(
        'label' => 'Top Offset',
        'input' => 'number',
        'value' => get_post_meta( $post->ID, 'top_offset', true ),
        'helps' => 'If provided, the image will be offset at the top to remove whitespace',
    );

    return $form_fields;
}

function top_offset_attachment_field_credit_save( $post, $attachment ) {
    if( isset( $attachment['top_offset'] ) )
        update_post_meta( $post['ID'], 'top_offset', $attachment['top_offset'] );

    return $post;
}

function ajaxhandler() {
    check_ajax_referer( 'my_secret_string', 'security', true );
    $attachment_id = $_POST['data'];
    $ids = array();

    foreach ($attachment_id as $id) {
        array_push($ids, get_post_meta($id, 'top_offset', true));
    }

    echo json_encode($ids);
    die();
}

function admin_scripts() {
    wp_register_script( 'whitespace_js', WHITESPACE_PLUGIN_URL . '/whitespace.js'    , array('jquery'), 1, true);
    wp_localize_script( 'whitespace_js', 'ws_js_glob', array(url => admin_url( 'admin-ajax.php' ), secret => wp_create_nonce( 'my_secret_string' ) ) );
    wp_enqueue_script(  'whitespace_js' );
}

function front_scripts() {
    wp_register_script( 'whitespace_front_js', WHITESPACE_PLUGIN_URL . '/whitespace_front.js'    , array('jquery'), 1, true);
    wp_enqueue_script(  'whitespace_front_js' );
} 

?>

That should add a new custom field in the media uploader to set the top offset on each image

media uploader field

It's a little complicated, but if you've ever written a plugin it should be straight forward.
This is not really tested, and it's just something I wrote to help you along the way, it could be improved a lot, and still needs some testing etc.

OTHER TIPS

I am working with something similar in my intranet application. Basically, I wrote a script in jquery that allows cropping the scanned image, so you can save only the part that is of interest to you.

What I can suggest like this, at first, since I am not adept at WordPress plugin programming, is that you do not actually crop the image if possible. You can offset it instead. So when you save the data from the moved image, you only save it's offset based on the initial position. In that case, the original image remains and you have the crop parameters by which you will offset the image (move it 100 px top, with top: -100px, for example).

If you really need to crop the image, you can do that with imagemagick (that is what I do in my application), but then it would include editing WordPress code and that I cannot help with very much.

Hope I helped a little bit at least.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top