Create a jQuery Image upload widget with preview and image cropping in Laravel


The user profile avatar upload option is pretty much standard in all websites which has user registration option nowadays. But many websites skip the image cropping options in profile photo upload module to avoid unwanted complications. But this results in stretched profile photo on their websites. This uncropped images also take more space of your valuable server storage. I this tutorial I will show you how to make a dead simple image upload widget with image cropping, rotating and real-time preview options only by using javascript and jquery in Laravel. I avoid javascript frameworks like React and Vue.js in this tutorial so that this tutorial will be useful to all PHP developers who know basics of javascript and jquery.

Before we start coding let's add all the libraries needed for making this widget in your laravel view page using CDN.

1) jQuery

You can grab the latest version of jQuery from the below CDN link.

<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>

2) Bootstrap 4

In this project, I am using the latest version of Bootstrap instead of old version. But you can use Bootstrap 3 if you are not comfortable with version 4. Here are the CDN links.

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" 
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" 
crossorigin="anonymous">

<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" 
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" 
crossorigin="anonymous"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" 
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" 
crossorigin="anonymous"></script>

 3) Croppie

The Croppie is a free and most popular open source image cropping plugin which can be used for building an image cropping widget in vanilla javascript. It is easy to use and support all popular browsers. You can grab croppie from following links.

<!-- Croppie css -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.2/croppie.min.css">

<!-- Croppie js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.2/croppie.min.js"></script>

4) Font Awesome 5

Since Bootstrap 4 drop support for glyphicons. Let's grab the best free icons collection package to improve the look and feel of our application. You can grab Font Awesome 5 from the below CDN.

<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css"
integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt"
crossorigin="anonymous">

Let's Start Coding

First, let's setup of the backend part of the demo applications as it is easier and straightforward than making frontend widgets. 

1) Configure your routes file

Add the following route to your web.php file

Route::get('demos/jquery-image-upload','[email protected]');
Route::post('demos/jquery-image-upload','[email protected]');

The first route is for showing the Initial View Page and the second route is a post route which is used by ajax call for uploading the cropped image to our server.

2) Setup your Controller 

Now Let's create our DemoController by running the following command.

php artisan make:controller DemoController

The complete code for DemoController is given below.

<?php

namespace App\Http\Controllers;

use Validator;
use Illuminate\Http\Request;

class DemoController extends Controller
{
    /**
     * To display the show page
     *
     * @return \Illuminate\Http\Response
     */
    public function showJqueryImageUpload() 
    {
        return view('demos.jqueryimageupload');
    }

    /**
     * To handle the comming post request
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function saveJqueryImageUpload(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'profile_picture' => 'required|image|max:1000',
        ]);

        if ($validator->fails()) {
            
            return $validator->errors();            
        }

        $status = "";

        if ($request->hasFile('profile_picture')) {
            $image = $request->file('profile_picture');
            // Rename image
            $filename = time().'.'.$image->guessExtension();
            
            $path = $request->file('profile_picture')->storeAs(
                 'profile_pictures', $filename
            );

            $status = "uploaded";            
        }
        
        return response($status,200);
    }

}‚Äč

The showJqueryImageUpload method is used to show our view page which has the image upload widget. 

The saveJqueryImageUpload method is used to upload the profile photo upload by the user. Normally we need to save the image name in the database. But in order to avoid unwanted complicity, I am avoiding this step. Remember store only the renamed image name and not entire URL of the uploaded image so that later you can easily change the location of uploaded images when your application needs server upgrade etc. One more tip Always grabs user id by using Auth helper function (Auth()->user()->id) instead of submitting it from the form as a hidden field.  Here I am renaming the image to for an extra layer of security. You can get the image extension from guessExtension method. I am storing the image in the uploads/profile_pictures folder which is inside the public folder. You can change your file upload path from the config/filesystem.php file.

In filesystem.php file

local' => [
            'driver' => 'local',
            'root' => public_path('uploads'),
        ],

3) The View Page

Now let's create the demo/jqueryimageupload.blade.php file to include our jquery widget. The complete code of this file is given below.

@extends('main')

@section('title','jQuery Image upload widget with preview and image cropping in Laravel - ')

@section('stylesheets')

<!-- Bootstrap 4 -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" 
    integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" 
    crossorigin="anonymous">
<!-- Croppie css -->
<link rel="stylesheet" type="text/css" 
    href="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.2/croppie.min.css">
<!-- Font Awesome 5 -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">



<style type="text/css">
.nounderline, .violet{
    color: #7c4dff !important;
}
.btn-dark {
    background-color: #7c4dff !important;
    border-color: #7c4dff !important;
}
.btn-dark .file-upload {
    width: 100%;
    padding: 10px 0px;
    position: absolute;
    left: 0;
    opacity: 0;
    cursor: pointer;
}
.profile-img img{
    width: 200px;
    height: 200px;
    border-radius: 50%;
}    
</style>

@endsection

@section('content')
<p>&nbsp;</p>
<div  class="container"> 
        
    <h3 class="text-center mb-5">
    jQuery Image upload widget with preview and image cropping in Laravel
    </h3>

    <div class="d-flex justify-content-center p-3">
        <div class="card text-center">
            <div class="card-body">
                <h5 class="card-title">Image Upload Widget</h5>
                <div class="profile-img p-3">
                    <img src="/images/icon-cam.png" id="profile-pic">
                </div>
                <div class="btn btn-dark">
                    <input type="file" class="file-upload" id="file-upload" 
                    name="profile_picture" accept="image/*">
                    Upload New Photo
                </div>
            </div>
        </div>
    </div>

    <!-- The Modal -->
    <div class="modal" id="myModal">
        <div class="modal-dialog modal-dialog-centered">
            <div class="modal-content">
                <!-- Modal Header -->
                <div class="modal-header">
                    <h4 class="modal-title">Crop Image And Upload</h4>
                    <button type="button" class="close" data-dismiss="modal">&times;</button>
                </div>
                <!-- Modal body -->
                <div class="modal-body">
                    <div id="resizer"></div>
                    <button class="btn rotate float-lef" data-deg="90" > 
                    <i class="fas fa-undo"></i></button>
                    <button class="btn rotate float-right" data-deg="-90" > 
                    <i class="fas fa-redo"></i></button>
                    <hr>
                    <button class="btn btn-block btn-dark" id="upload" > 
                    Crop And Upload</button>
                </div>
            </div>
        </div>
    </div>
    
    <div class="mt-5 mb-5">
        <hr>
        <h5>For the complete tutorial of how to make this demo app visit the following 
            <a class="violet" href="">Link</a>.
        </h5>
    </div>


</div>
    
@stop

@section('scripts')

<!--  jQuery and Popper.js  -->
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
    integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
    crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" 
    integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" 
    crossorigin="anonymous"></script>
<!-- Boostrap 4 -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" 
    integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" 
    crossorigin="anonymous"></script>
<!-- Croppie js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.2/croppie.min.js"></script>

<script type="text/javascript">
  <!-- Refer Below for Javascript Code -->
</script>
    
@stop

The styles written in this demo applications is to give a button like an appearance to the File upload field and to change the button colour to my website theme colour. I also give some style to give a circular appearance to the avatar. Feel free to customize according to your application.

I used the new flex class and Card class of Bootstrap 4 to centralize the avatar widget and to give a small border around it. When the user clicks on Upload New Photo File upload field, a Bootstrap model will pop up with options to crop and rotate the image. Once you upload the image the avatar icon will be changed to uploaded cropped image. 

4) The Javascript Part

First, we need to listen to the change event in our File Input Field and Initialize Croppie as below.

$("#file-upload").on("change", function(event) {
    $("#myModal").modal();
    // Initailize croppie instance and assign it to global variable
    croppie = new Croppie(el, {
                viewport: {
                    width: 200,
                    height: 200,
                    type: 'circle'
                },
                boundary: {
                    width: 250,
                    height: 250
                },
                enableOrientation: true
             });
    $.getImage(event.target, croppie); 
});

The first part of croppie will accept the HTML element in which the Croppie appends and second part accepts an object of options we used to customize the Croppie. The viewport defines the height and width of the cropped image and boundary define the height and width of the boundary around the viewpoint. The enableOrientation will allows us to rotate the image. The Croppie is initialized and assigned to a global variable - croppie for further processing.

The $.getImage custom method is used to read the selected image from file input and bind it to Croppie for cropping it. 

$.getImage = function(input, croppie) {
        if (input.files && input.files[0]) {
            var reader = new FileReader();
            reader.onload = function(e) {  
                croppie.bind({
                    url: e.target.result,
                });
            }
            reader.readAsDataURL(input.files[0]);
        }
}

The FileReader object lets web applications asynchronously read the contents of files and make a base 64 version of the image which can be given to croppie for further processing. The readAsDataURL method is used to read the contents of the specified Blob or File.

You can rotate the image left or right as below.

$(".rotate").on("click", function() {
    croppie.rotate(parseInt($(this).data('deg'))); 
});

Now once the User click Crop And Upload Button we need can grab the cropped image and upload it to the server by using jquery Ajax as below. 

$("#upload").on("click", function() {

    croppie.result('base64').then(function(base64) {
        $("#myModal").modal("hide"); 
        $("#profile-pic").attr("src","/images/ajax-loader.gif");

        var url = "{{ url('/demos/jquery-image-upload') }}";
        var formData = new FormData();
        formData.append("profile_picture", $.base64ImageToBlob(base64));

        // This step is only needed if you are using Laravel
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });

        $.ajax({
                type: 'POST',
                url: url,
                data: formData,
                processData: false,
                contentType: false,
                success: function(data) {
                    if (data == "uploaded") {
                        $("#profile-pic").attr("src", base64); 
                    } else {
                        $("#profile-pic").attr("src","/images/icon-cam.png"); 
                        console.log(data['profile_picture']);
                    }
                },
                error: function(error) {
                    console.log(error);
                    $("#profile-pic").attr("src","/images/icon-cam.png"); 
                }
        });
    });
});

The croppie.result('base64') will give a base 64 version of the cropped image. We need to convert it to Blob format and since we are uploading a file we also need to upload data as a FormData. We need to set processData false to send DOMDocument or other non-processed data. Since we are uploading multipart/form-data we also need to set contentType false. On the successful upload of the image, we replace the avatar icon with the base64 version of the uploaded image. 

Since we are using Laravel, we need to pass the csrf token along with the Ajax request. You can pass X-CSRF-TOKEN token in Ajax Header as below.

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

Here we grab the csrf token from the csrf-token meta tag. If you don't have this meta tag then paste below code to the header session of your page.

<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">

Do not forget to destroy the Croppie instance on the model close so that we can upload and crop a new image every time as below.

$('#myModal').on('hidden.bs.modal', function (e) {
   // This function will call immediately after model close
   // To ensure that old croppie instance is destroyed on every model close
   setTimeout(function() { croppie.destroy(); }, 100);
})

I destroy the Croppie instance after 100ms so that we can grab the cropped result first before the instance is destroyed. 

You can get the Blob version of the cropped image from $.base64ImageToBlob method.

$.base64ImageToBlob = function(str) {
    // extract content type and base64 payload from original string
    var pos = str.indexOf(';base64,');
    var type = str.substring(5, pos);
    var b64 = str.substr(pos + 8);
  
    // decode base64
    var imageContent = atob(b64);
  
    // create an ArrayBuffer and a view (as unsigned 8-bit)
    var buffer = new ArrayBuffer(imageContent.length);
    var view = new Uint8Array(buffer);
  
    // fill the view, using the decoded base64
    for (var n = 0; n < imageContent.length; n++) {
      view[n] = imageContent.charCodeAt(n);
    }
  
    // convert ArrayBuffer to Blob
    var blob = new Blob([buffer], { type: type });
  
    return blob;
}

Now let's see The Complete Version of our Javascript part of this demo application.

$(function() {
    var croppie = null;
    var el = document.getElementById('resizer');

    $.base64ImageToBlob = function(str) {
        // extract content type and base64 payload from original string
        var pos = str.indexOf(';base64,');
        var type = str.substring(5, pos);
        var b64 = str.substr(pos + 8);
      
        // decode base64
        var imageContent = atob(b64);
      
        // create an ArrayBuffer and a view (as unsigned 8-bit)
        var buffer = new ArrayBuffer(imageContent.length);
        var view = new Uint8Array(buffer);
      
        // fill the view, using the decoded base64
        for (var n = 0; n < imageContent.length; n++) {
          view[n] = imageContent.charCodeAt(n);
        }
      
        // convert ArrayBuffer to Blob
        var blob = new Blob([buffer], { type: type });
      
        return blob;
    }

    $.getImage = function(input, croppie) {
        if (input.files && input.files[0]) {
            var reader = new FileReader();
            reader.onload = function(e) {  
                croppie.bind({
                    url: e.target.result,
                });
            }
            reader.readAsDataURL(input.files[0]);
        }
    }

    $("#file-upload").on("change", function(event) {
        $("#myModal").modal();
        // Initailize croppie instance and assign it to global variable
        croppie = new Croppie(el, {
                viewport: {
                    width: 200,
                    height: 200,
                    type: 'circle'
                },
                boundary: {
                    width: 250,
                    height: 250
                },
                enableOrientation: true
            });
        $.getImage(event.target, croppie); 
    });

    $("#upload").on("click", function() {
        croppie.result('base64').then(function(base64) {
            $("#myModal").modal("hide"); 
            $("#profile-pic").attr("src","/images/ajax-loader.gif");

            var url = "{{ url('/demos/jquery-image-upload') }}";
            var formData = new FormData();
            formData.append("profile_picture", $.base64ImageToBlob(base64));

            // This step is only needed if you are using Laravel
            $.ajaxSetup({
                headers: {
                    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                }
            });

            $.ajax({
                type: 'POST',
                url: url,
                data: formData,
                processData: false,
                contentType: false,
                success: function(data) {
                    if (data == "uploaded") {
                        $("#profile-pic").attr("src", base64); 
                    } else {
                        $("#profile-pic").attr("src","/images/icon-cam.png"); 
                        console.log(data['profile_picture']);
                    }
                },
                error: function(error) {
                    console.log(error);
                    $("#profile-pic").attr("src","/images/icon-cam.png"); 
                }
            });
        });
    });

    // To Rotate Image Left or Right
    $(".rotate").on("click", function() {
        croppie.rotate(parseInt($(this).data('deg'))); 
    });

    $('#myModal').on('hidden.bs.modal', function (e) {
        // This function will call immediately after model close
        // To ensure that old croppie instance is destroyed on every model close
        setTimeout(function() { croppie.destroy(); }, 100);
    })

});

The output image of the above demo project is given below.

jQuery Image upload widget with preview and image cropping in Laravel

The Demo

You can demo the above application by visiting the following link.

https://shareurcodes.com/demos/jquery-image-upload

If anybody has any suggestions or doubts or need any help comment below and I try will respond to every one of you as early as possible.


Web development
15th Jul 2018 11:09:00 PM
PHP Laravel Javascript jQuery
1425

ShareurCodes

ShareurCodes is a code sharing site for programmers to learn, share their knowledge with younger generation.