2

I've followed this article to use Media Uploader in my code. When I click the Browse button on my form, the media uploader opens and after selecting the image, I get the URL of that image added in my form field. No issues so far.

But I want to enforce the image to be of a specific size. If the size is different, user must crop it to match the required size (or aspect ratio) before he can select the image (and my mediaUploader.on('select', function() { is executed).

Please note that I don't want to use add_image_size because that will create a thumbnail of the given size for each and every image uploaded (AFAIK... correct me if I'm wrong). Instead, only in a specific scenario I want the user to crop the image manually while uploading.

Basically my requirement is well described in a question on SO: wordpress-media-library-crop-image-on-insert-like-header-image-cropper

But there is no solution posted there. So I'm putting it here for the WP experts to guide me.

I've tried searching for 'wordpress media uploader (enforce image size | image cropper | select image of specific size)' and what not. But somehow not getting any link that describes the steps to enforce a given image size. Nor could I found any documentation for wp.media that describes all the options that can be passed.

I've seen following questions already but that doesn't seem to do what I'm looking for:

Edit

I could find the use of WP_Customize_Cropped_Image_Control @ https://www.sitepoint.com/using-the-wordpress-customizer-media-controls/ But its in the context of Customizer. Is there any similar control that can be used in standard plugin?

And there is this wp.media.controller.Cropper defined inside wp-includes/js/media-views.js but how do I use it?

3

First use the code given by @Darren on any page. For simplicity I've combined PHP and JS code in single block:

  <?php $heading_picture = esc_attr(get_option( 'heading_picture' )); ?>
  <input type="hidden" name="heading_picture" id="heading_picture" value="<?php echo $heading_picture; ?>" />
  <img id="heading_picture_preview" class="heading-picture" src="<?php echo $heading_picture; ?>" />
  <button id="btn_heading_picture" name="btn_heading_picture" class="button default">Choose Picture</button>
<script>
jQuery(function($) {

function myTheme_calculateImageSelectOptions(attachment, controller) {

    var control = controller.get( 'control' );

    var flexWidth = !! parseInt( control.params.flex_width, 10 );
    var flexHeight = !! parseInt( control.params.flex_height, 10 );

    var realWidth = attachment.get( 'width' );
    var realHeight = attachment.get( 'height' );

    var xInit = parseInt(control.params.width, 10);
    var yInit = parseInt(control.params.height, 10);

    var ratio = xInit / yInit;

    controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );

    var xImg = xInit;
    var yImg = yInit;

    if ( realWidth / realHeight > ratio ) {
        yInit = realHeight;
        xInit = yInit * ratio;
    } else {
        xInit = realWidth;
        yInit = xInit / ratio;
    }        

    var x1 = ( realWidth - xInit ) / 2;
    var y1 = ( realHeight - yInit ) / 2;        

    var imgSelectOptions = {
        handles: true,
        keys: true,
        instance: true,
        persistent: true,
        imageWidth: realWidth,
        imageHeight: realHeight,
        minWidth: xImg > xInit ? xInit : xImg,
        minHeight: yImg > yInit ? yInit : yImg,            
        x1: x1,
        y1: y1,
        x2: xInit + x1,
        y2: yInit + y1
    };

    return imgSelectOptions;
}  

function myTheme_setImageFromURL(url, attachmentId, width, height) {
    var choice, data = {};

    data.url = url;
    data.thumbnail_url = url;
    data.timestamp = _.now();

    if (attachmentId) {
        data.attachment_id = attachmentId;
    }

    if (width) {
        data.width = width;
    }

    if (height) {
        data.height = height;
    }

    $("#heading_picture").val( url );
    $("#heading_picture_preview").prop("src", url);        

}

function myTheme_setImageFromAttachment(attachment) {

    $("#heading_picture").val( attachment.url );
    $("#heading_picture_preview").prop("src", attachment.url);             

}

var mediaUploader;

$("#btn_heading_picture").on("click", function(e) {

    e.preventDefault(); 

    /* We need to setup a Crop control that contains a few parameters
       and a method to indicate if the CropController can skip cropping the image.
       In this example I am just creating a control on the fly with the expected properties.
       However, the controls used by WordPress Admin are api.CroppedImageControl and api.SiteIconControl
    */

   var cropControl = {
       id: "control-id",
       params : {
         flex_width : false,  // set to true if the width of the cropped image can be different to the width defined here
         flex_height : true, // set to true if the height of the cropped image can be different to the height defined here
         width : 300,  // set the desired width of the destination image here
         height : 200, // set the desired height of the destination image here
       }
   };

   cropControl.mustBeCropped = function(flexW, flexH, dstW, dstH, imgW, imgH) {

    // If the width and height are both flexible
    // then the user does not need to crop the image.

    if ( true === flexW && true === flexH ) {
        return false;
    }

    // If the width is flexible and the cropped image height matches the current image height, 
    // then the user does not need to crop the image.
    if ( true === flexW && dstH === imgH ) {
        return false;
    }

    // If the height is flexible and the cropped image width matches the current image width, 
    // then the user does not need to crop the image.        
    if ( true === flexH && dstW === imgW ) {
        return false;
    }

    // If the cropped image width matches the current image width, 
    // and the cropped image height matches the current image height
    // then the user does not need to crop the image.               
    if ( dstW === imgW && dstH === imgH ) {
        return false;
    }

    // If the destination width is equal to or greater than the cropped image width
    // then the user does not need to crop the image...
    if ( imgW <= dstW ) {
        return false;
    }

    return true;        

   };      

    /* NOTE: Need to set this up every time instead of reusing if already there
             as the toolbar button does not get reset when doing the following:

            mediaUploader.setState('library');
            mediaUploader.open();

    */       

    mediaUploader = wp.media({
        button: {
            text: 'Select and Crop', // l10n.selectAndCrop,
            close: false
        },
        states: [
            new wp.media.controller.Library({
                title:     'Select and Crop', // l10n.chooseImage,
                library:   wp.media.query({ type: 'image' }),
                multiple:  false,
                date:      false,
                priority:  20,
                suggestedWidth: 300,
                suggestedHeight: 200
            }),
            new wp.media.controller.CustomizeImageCropper({ 
                imgSelectOptions: myTheme_calculateImageSelectOptions,
                control: cropControl
            })
        ]
    });

    mediaUploader.on('cropped', function(croppedImage) {

        var url = croppedImage.url,
            attachmentId = croppedImage.attachment_id,
            w = croppedImage.width,
            h = croppedImage.height;

            myTheme_setImageFromURL(url, attachmentId, w, h);            

    });

    mediaUploader.on('skippedcrop', function(selection) {

        var url = selection.get('url'),
            w = selection.get('width'),
            h = selection.get('height');

            myTheme_setImageFromURL(url, selection.id, w, h);            

    });        

    mediaUploader.on("select", function() {

        var attachment = mediaUploader.state().get( 'selection' ).first().toJSON();

        if (     cropControl.params.width  === attachment.width 
            &&   cropControl.params.height === attachment.height 
            && ! cropControl.params.flex_width 
            && ! cropControl.params.flex_height ) {
                myTheme_setImageFromAttachment( attachment );
            mediaUploader.close();
        } else {
            mediaUploader.setState( 'cropper' );
        }

    });

    mediaUploader.open();

});
});
</script>

Then in some plugin use following code:

add_action( 'wp_enqueue_scripts', 'vna_wp_enqueue_scripts' );
function vna_wp_enqueue_scripts() {
    wp_enqueue_media();
    wp_enqueue_script( 'imgareaselect', get_bloginfo('url') . '/wp-includes/js/imgareaselect/jquery.imgareaselect.js', array( 'jquery' ), '1', true );
    wp_enqueue_style( 'imgareaselect', get_bloginfo('url') . '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.8' );
}
add_action( 'setup_theme','vna_wp_ajax_crop_image', 1 );
function vna_wp_ajax_crop_image() {
    global $wp_customize;
    if(isset($_REQUEST['action']) && $_REQUEST['action'] == 'crop-image') {
        $post = new WP_Post(new stdClass());
        $post->ID = $_REQUEST['id'];
        $post->post_type = 'customize_changeset';
        $post->filter = 'raw';

        wp_cache_set($wp_customize->changeset_uuid(), $post, 'customize_changeset_post');
    }
}

Notes:

  1. There is lot of dependency on admin JS files. Earlier I was trying to add these files one after other based on the JS errors I was getting during testing. Then I found wp_enqueue_media() which does most of the work. It still doesn't load imgareaselect JS and CSS, so we have to load them explicitly.
  2. The above code must be in a plugin. I was trying the code in a theme and it was not working because the setup_theme action in theme gets called after the expected event (explained below). To avoid it I had to put it in plugin.
  3. Cropping functionality works only when a user is logged in and has edit_post permission on the image. If not, you will get 0 as the response from admin-ajax.php when the action is crop-image.
  4. The hacky way: Execute the above code on setup_theme action with priority set to 1. This is because class WP_Customize_Manager has a setup_theme method that gets executed and checks for the presence of a post id / post which is of type customize_changeset. Also the post filter needs to be raw otherwise it tries to load the post from DB for the given post ID (in this case we are setting the image id as the post ID) which results in post type getting set to attachment. If the check fails (it doesn't find a post for given ID that is of type customize_changeset) then you get -1 as the response from admin-ajax.php when the action is crop-image. Instead of creating a valid changeset post in DB, I just created a dummy post object and added it to cache.

The code doesn't look much but as I mentioned earlier in my comment it was really a pain to find out all these conditions. I'm glad I managed it afterall.

2

It looks like the select and crop functionality is currently only used in the WordPress Customizer.

However, after looking over the code in the Admin folder of WordPress I was able to get it working on my themes option page.

Here is my setting for a header picture:

function setting_heading_picture() { 
  $heading_picture = esc_attr(get_option( 'heading_picture' )); ?>
  <input type="hidden" name="heading_picture" id="heading_picture" value="<?php echo $heading_picture; ?>" />
  <img id="heading_picture_preview" class="heading-picture" src="<?php echo $heading_picture; ?>" />
  <button id="btn_heading_picture" name="btn_heading_picture" class="button default">Choose Picture</button>
}

Now here is the admin javascript required:

$(function() {

function myTheme_calculateImageSelectOptions(attachment, controller) {

    var control = controller.get( 'control' );

    var flexWidth = !! parseInt( control.params.flex_width, 10 );
    var flexHeight = !! parseInt( control.params.flex_height, 10 );

    var realWidth = attachment.get( 'width' );
    var realHeight = attachment.get( 'height' );

    var xInit = parseInt(control.params.width, 10);
    var yInit = parseInt(control.params.height, 10);

    var ratio = xInit / yInit;

    controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );

    var xImg = xInit;
    var yImg = yInit;

    if ( realWidth / realHeight > ratio ) {
        yInit = realHeight;
        xInit = yInit * ratio;
    } else {
        xInit = realWidth;
        yInit = xInit / ratio;
    }        

    var x1 = ( realWidth - xInit ) / 2;
    var y1 = ( realHeight - yInit ) / 2;        

    var imgSelectOptions = {
        handles: true,
        keys: true,
        instance: true,
        persistent: true,
        imageWidth: realWidth,
        imageHeight: realHeight,
        minWidth: xImg > xInit ? xInit : xImg,
        minHeight: yImg > yInit ? yInit : yImg,            
        x1: x1,
        y1: y1,
        x2: xInit + x1,
        y2: yInit + y1
    };

    return imgSelectOptions;
}  

function myTheme_setImageFromURL(url, attachmentId, width, height) {
    var choice, data = {};

    data.url = url;
    data.thumbnail_url = url;
    data.timestamp = _.now();

    if (attachmentId) {
        data.attachment_id = attachmentId;
    }

    if (width) {
        data.width = width;
    }

    if (height) {
        data.height = height;
    }

    $("#heading_picture").val( url );
    $("#heading_picture_preview").prop("src", url);        

}

function myTheme_setImageFromAttachment(attachment) {

    $("#heading_picture").val( attachment.url );
    $("#heading_picture_preview").prop("src", attachment.url);             

}

var mediaUploader;

$("#btn_heading_picture").on("click", function(e) {

    e.preventDefault(); 

    /* We need to setup a Crop control that contains a few parameters
       and a method to indicate if the CropController can skip cropping the image.
       In this example I am just creating a control on the fly with the expected properties.
       However, the controls used by WordPress Admin are api.CroppedImageControl and api.SiteIconControl
    */

   var cropControl = {
       id: "control-id",
       params : {
         flex_width : false,  // set to true if the width of the cropped image can be different to the width defined here
         flex_height : true, // set to true if the height of the cropped image can be different to the height defined here
         width : 300,  // set the desired width of the destination image here
         height : 200, // set the desired height of the destination image here
       }
   };

   cropControl.mustBeCropped = function(flexW, flexH, dstW, dstH, imgW, imgH) {

    // If the width and height are both flexible
    // then the user does not need to crop the image.

    if ( true === flexW && true === flexH ) {
        return false;
    }

    // If the width is flexible and the cropped image height matches the current image height, 
    // then the user does not need to crop the image.
    if ( true === flexW && dstH === imgH ) {
        return false;
    }

    // If the height is flexible and the cropped image width matches the current image width, 
    // then the user does not need to crop the image.        
    if ( true === flexH && dstW === imgW ) {
        return false;
    }

    // If the cropped image width matches the current image width, 
    // and the cropped image height matches the current image height
    // then the user does not need to crop the image.               
    if ( dstW === imgW && dstH === imgH ) {
        return false;
    }

    // If the destination width is equal to or greater than the cropped image width
    // then the user does not need to crop the image...
    if ( imgW <= dstW ) {
        return false;
    }

    return true;        

   };      

    /* NOTE: Need to set this up every time instead of reusing if already there
             as the toolbar button does not get reset when doing the following:

            mediaUploader.setState('library');
            mediaUploader.open();

    */       

    mediaUploader = wp.media({
        button: {
            text: 'Select and Crop', // l10n.selectAndCrop,
            close: false
        },
        states: [
            new wp.media.controller.Library({
                title:     'Select and Crop', // l10n.chooseImage,
                library:   wp.media.query({ type: 'image' }),
                multiple:  false,
                date:      false,
                priority:  20,
                suggestedWidth: 300,
                suggestedHeight: 200
            }),
            new wp.media.controller.CustomizeImageCropper({ 
                imgSelectOptions: myTheme_calculateImageSelectOptions,
                control: cropControl
            })
        ]
    });

    mediaUploader.on('cropped', function(croppedImage) {

        var url = croppedImage.url,
            attachmentId = croppedImage.attachment_id,
            w = croppedImage.width,
            h = croppedImage.height;

            myTheme_setImageFromURL(url, attachmentId, w, h);            

    });

    mediaUploader.on('skippedcrop', function(selection) {

        var url = selection.get('url'),
            w = selection.get('width'),
            h = selection.get('height');

            myTheme_setImageFromURL(url, selection.id, w, h);            

    });        

    mediaUploader.on("select", function() {

        var attachment = mediaUploader.state().get( 'selection' ).first().toJSON();

        if (     cropControl.params.width  === attachment.width 
            &&   cropControl.params.height === attachment.height 
            && ! cropControl.params.flex_width 
            && ! cropControl.params.flex_height ) {
                myTheme_setImageFromAttachment( attachment );
            mediaUploader.close();
        } else {
            mediaUploader.setState( 'cropper' );
        }

    });

    mediaUploader.open();

});
});

Don't forget to adjust the desired width / height that you need in the code above.

Code to save the cropped image as an attachment has been omitted. The cropped images will still be in the usual place in wp-content/uploads but you won't see the cropped images in the Media Library.

I don't know how to enforce an exact size in the Cropper. Hopefully someone else can come along and help answer that.

  • thanks for your efforts. I'm on vacation for next couple of weeks. I'll try this out once I'm back. – Vivek Athalye May 9 '18 at 1:25
  • @VivekAthalye did you managed to figure out the solution? – Manoj H L Aug 17 '18 at 3:36
  • 1
    @ManojHL As Darren had mentioned he was able to use it in Admin area, whereas I wanted to use it outside admin area. So when I tried it last time it did not work and I had to put it on low priority because of some other high priority work. Now that you asked if it worked, and I had some bandwidth, I decided to give it a shot again. And the good news is I've managed to get the cropping functionality working outside admin area, in a bit hacky way. It was really a pain. I'll need some time to cleanup my code and document it properly. Hopefully will add an answer in couple of days. – Vivek Athalye Aug 22 '18 at 8:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.