Upload and edit image using Croppic jQuery plugin

I often have to code image upload widgets for profile photos or some kind of image editing feature. Each website layout uses custom photo sizes and cropping those image on server side could cause image distortion. Because of this I like to put image editing on client side and recently I discovered one new pretty simple plugin, Croppic.

It works in similar way as Twitter, Facebook or Linkedin profile photo widgets. First you select wanted image, after that you are presented with slide and zoom options, and when you are satisfied you click crop. Could not be any simpler than this!

The best part is that you set fixed size of wanted image, and your users only need to slide cropping mask over original image. There are also some more advanced options, like rotating, but you can remove them.

Croppic in action

Croppic works in following way:

  • when you select image from browse window it will be uploaded to server, in original form
  • server responds with url to newly uploaded image and Croppic renders it
  • user can slide image, zoom-in, zoom-out and after he clicks crop button data is sent to server
  • server receives url of original image, and cropping details like: x-position, y-position, cropped width, cropped height, angle.
  • after server processes image using cropping details, it sends success response to client
  • if any errors occur, alert dialog is displayed with error message
  • after successful cropping, final image is displayed to user in Croppic box
  • user can click X and start process all over again

In this tutorial I will be using Image Intervention package, for server side image processing.

You can find entire code used in this tutorial on Github.

Installing and configuring Laravel project (skip if you know how to setup the project)

Before you continue with the tutorial create new laravel application with laravel new croppic, copy .env.example file to .env and add needed values to that file. For me these values in .env are:


URL=http://croppic.dev/
UPLOAD_PATH=/var/www/html/tuts/croppic/public/uploads/

After this generate new application key with: php artisan key:generate

Now we need to install Image Intervention, type: composer require intervention/image

You may need to apply some modifications to permissions of storage and bootstrap folders, but you should be ready to go.

Croppic Options

You can configure almost everything with JS options array. Croppic can be displayed as built-in modal, you can pass custom data to backend, define zoom/rotate factors, define image output element, or custom upload button.

It can to initial image upload on client side using FileReader API, so you can skip first 2 items from above list. But there is one downside of this solution - some browsers don't support FileReader API.

In this example I will define upload and crop URLs, and manually send crop mask width and height.


var eyeCandy = $('#cropContainerEyecandy');
var croppedOptions = {
    uploadUrl: 'upload',
    cropUrl: 'crop',
    cropData:{
        'width' : eyeCandy.width(),
        'height': eyeCandy.height()
    }
};
var cropperBox = new Croppic('cropContainerEyecandy', croppedOptions);

Variable eyeCandy is holding reference to DOM element where I want to render Croppic. In croppedOptions I am using jQuery to take dimensions of eyeCandy element. I am calculating dimensions here, cause I am using Bootstrap grid in frontend, so width and height vary based on window size.

Frontend

As mentioned, I am using Bootstrap and cause I am lazy I downloaded custom styles from Croppic site.

    
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Upload and edit images in Laravel using Croppic jQuery plugin</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"/>
    <link rel="stylesheet" href="plugins/croppic/assets/css/main.css"/>
    <link rel="stylesheet" href="plugins/croppic/assets/css/croppic.css"/>

    <link href='http://fonts.googleapis.com/css?family=Lato:300,400,900' rel='stylesheet' type='text/css'>
    <link href='http://fonts.googleapis.com/css?family=Mrs+Sheppards&subset=latin,latin-ext' rel='stylesheet' type='text/css'>

</head>
<body>

<div class="container">
    <div class="row margin-bottom-40">
        <div class="col-md-12">
            <h1>Upload and edit images in Laravel using Croppic jQuery plugin</h1>
        </div>
    </div>

    <div class="row margin-bottom-40">
        <div class=" col-md-3">
            <div id="cropContainerEyecandy"></div>
        </div>
    </div>

    <div class="row">
        <div class="col-md-12">
            <p><a href="http://www.croppic.net/" target="_blank">Croppic</a> is ideal for uploading profile photos,
        or photos where you require predefined size/ratio.</p>
        </div>
    </div>
</div>
<script src=" https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script src="plugins/croppic/croppic.min.js"></script>
<script>
    var eyeCandy = $('#cropContainerEyecandy');
    var croppedOptions = {
        uploadUrl: 'upload',
        cropUrl: 'crop',
        cropData:{
            'width' : eyeCandy.width(),
            'height': eyeCandy.height()
        }
    };
    var cropperBox = new Croppic('cropContainerEyecandy', croppedOptions);
</script>
</body>
</html>
    

Routes

We will need 3 routes, one for home page, one for upload post request, and one for crop post request.


<?php

Route::get('/', 'CropController@getHome');
Route::post('upload', 'CropController@postUpload');
Route::post('crop', 'CropController@postCrop');

From experience I know Laravel with throw CSRF token error, so I will add 2 exception in CSRF middleware for crop and upload routes.


<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;

class VerifyCsrfToken extends BaseVerifier
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'upload',
        'crop'
    ];
}

Backend Logic

Image Model and Migration

I am using database here cause I want to keep track of files uploaded. I usually have relation table between images and users, with that part everything will make much more sense.

    
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
    protected $table = 'images';

    public static $rules = [
        'img' => 'required|mimes:png,gif,jpeg,jpg,bmp'
    ];
    public static $messages = [
        'img.mimes' => 'Uploaded file is not in image format',
        'img.required' => 'Image is required'
    ];
}
    

As always I am keeping models in separated folder in app/Models

    
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateImages extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('images', function (Blueprint $table) {
            $table->increments('id');
            $table->text('original_name');
            $table->text('filename');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('images');
    }
}
        

You should create new database and database user and insert those credentials into .env file. After you have done you can run migration: php artisan migrate

Upload photo logic

This method is called immediately after user selects wanted photo from browse dialog.

        
        public function postUpload()
    {
        $form_data = Input::all();

        $validator = Validator::make($form_data, Image::$rules, Image::$messages);

        if ($validator->fails()) {

            return Response::json([
                'status' => 'error',
                'message' => $validator->messages()->first(),
            ], 200);

        }

        $photo = $form_data['img'];

        $original_name = $photo->getClientOriginalName();
        $original_name_without_ext = substr($original_name, 0, strlen($original_name) - 4);

        $filename = $this->sanitize($original_name_without_ext);
        $allowed_filename = $this->createUniqueFilename( $filename );

        $filename_ext = $allowed_filename .'.jpg';

        $manager = new ImageManager();
        $image = $manager->make( $photo )->encode('jpg')->save(env('UPLOAD_PATH') . $filename_ext );

        if( !$image) {

            return Response::json([
                'status' => 'error',
                'message' => 'Server error while uploading',
            ], 200);

        }

        $database_image = new Image;
        $database_image->filename      = $allowed_filename;
        $database_image->original_name = $original_name;
        $database_image->save();

        return Response::json([
            'status'    => 'success',
            'url'       => env('URL') . 'uploads/' . $filename_ext,
            'width'     => $image->width(),
            'height'    => $image->height()
        ], 200);
    }

First I am validating input using validation arrays from Image model. There I've specified image formats and stated that image is required. You can also add other constraints, like image size etc.

If validation fails, backend will send error response, and Croppic will throw alert dialog.

One note: Ordinary alert window looks pretty ugly, so I alway use SweetAlert. Just search for alert in croppic.js and replace that line with: sweetAlert("Oops...", response.message, 'error'); Of course you will need to include SweetAlert css and js files in HTML.

I am using sanitize and createUniqueFilename methods to create server side filename. Usualy I would create ImageRepository and host all these methods there, but this way is simpler.

    
    private function sanitize($string, $force_lowercase = true, $anal = false)
    {
        $strip = array("~", "`", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "=", "+", "[", "{", "]",
            "}", "\\", "|", ";", ":", "\"", "'", "&#8216;", "&#8217;", "&#8220;", "&#8221;", "&#8211;", "&#8212;",
            "—", "–", ",", "<", ".", ">", "/", "?");
        $clean = trim(str_replace($strip, "", strip_tags($string)));
        $clean = preg_replace('/\s+/', "-", $clean);
        $clean = ($anal) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean ;

        return ($force_lowercase) ?
            (function_exists('mb_strtolower')) ?
                mb_strtolower($clean, 'UTF-8') :
                strtolower($clean) :
            $clean;
    }


    private function createUniqueFilename( $filename )
    {
        $upload_path = env('UPLOAD_PATH');
        $full_image_path = $upload_path . $filename . '.jpg';

        if ( File::exists( $full_image_path ) )
        {
            // Generate token for image
            $image_token = substr(sha1(mt_rand()), 0, 5);
            return $filename . '-' . $image_token;
        }

        return $filename;
    }
    

After creating unique filename, I am using Image Intervention ImageManger to save uploaded image. As response from upload method, Croppic expects: status, url, width and height of stored image.

Crop photo logic

After user clicks on crop button Croppic will send data to backend route for cropping image. Actually Croppic is not doing any cropping :-) it only sends x/y coordinates of moved image and width and height of cropping mask. We will have to code cropping logic in the backend. Croppic project provides some basic php script for this, but I am using Image Intervention package again here.

    
    public function postCrop()
    {
        $form_data = Input::all();
        $image_url = $form_data['imgUrl'];

        // resized sizes
        $imgW = $form_data['imgW'];
        $imgH = $form_data['imgH'];
        // offsets
        $imgY1 = $form_data['imgY1'];
        $imgX1 = $form_data['imgX1'];
        // crop box
        $cropW = $form_data['width'];
        $cropH = $form_data['height'];
        // rotation angle
        $angle = $form_data['rotation'];

        $filename_array = explode('/', $image_url);
        $filename = $filename_array[sizeof($filename_array)-1];

        $manager = new ImageManager();
        $image = $manager->make( $image_url );
        $image->resize($imgW, $imgH)
            ->rotate(-$angle)
            ->crop($cropW, $cropH, $imgX1, $imgY1)
            ->save(env('UPLOAD_PATH') . 'cropped-' . $filename);

        if( !$image) {

            return Response::json([
                'status' => 'error',
                'message' => 'Server error while uploading',
            ], 200);

        }

        return Response::json([
            'status' => 'success',
            'url' => env('URL') . 'uploads/cropped-' . $filename
        ], 200);

    }
    

Full CropController looks like this:


<?php

namespace App\Http\Controllers;

use App\Models\Image;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response;
use Intervention\Image\ImageManager;
use Illuminate\Support\Facades\File;

class CropController extends Controller{

    public function getHome()
    {
        return view('home');
    }

    public function postUpload()
    {
        $form_data = Input::all();

        $validator = Validator::make($form_data, Image::$rules, Image::$messages);

        if ($validator->fails()) {

            return Response::json([
                'status' => 'error',
                'message' => $validator->messages()->first(),
            ], 200);

        }

        $photo = $form_data['img'];

        $original_name = $photo->getClientOriginalName();
        $original_name_without_ext = substr($original_name, 0, strlen($original_name) - 4);

        $filename = $this->sanitize($original_name_without_ext);
        $allowed_filename = $this->createUniqueFilename( $filename );

        $filename_ext = $allowed_filename .'.jpg';

        $manager = new ImageManager();
        $image = $manager->make( $photo )->encode('jpg')->save(env('UPLOAD_PATH') . $filename_ext );

        if( !$image) {

            return Response::json([
                'status' => 'error',
                'message' => 'Server error while uploading',
            ], 200);

        }

        $database_image = new Image;
        $database_image->filename      = $allowed_filename;
        $database_image->original_name = $original_name;
        $database_image->save();

        return Response::json([
            'status'    => 'success',
            'url'       => env('URL') . 'uploads/' . $filename_ext,
            'width'     => $image->width(),
            'height'    => $image->height()
        ], 200);
    }


    public function postCrop()
    {
        $form_data = Input::all();
        $image_url = $form_data['imgUrl'];

        // resized sizes
        $imgW = $form_data['imgW'];
        $imgH = $form_data['imgH'];
        // offsets
        $imgY1 = $form_data['imgY1'];
        $imgX1 = $form_data['imgX1'];
        // crop box
        $cropW = $form_data['width'];
        $cropH = $form_data['height'];
        // rotation angle
        $angle = $form_data['rotation'];

        $filename_array = explode('/', $image_url);
        $filename = $filename_array[sizeof($filename_array)-1];

        $manager = new ImageManager();
        $image = $manager->make( $image_url );
        $image->resize($imgW, $imgH)
            ->rotate(-$angle)
            ->crop($cropW, $cropH, $imgX1, $imgY1)
            ->save(env('UPLOAD_PATH') . 'cropped-' . $filename);

        if( !$image) {

            return Response::json([
                'status' => 'error',
                'message' => 'Server error while uploading',
            ], 200);

        }

        return Response::json([
            'status' => 'success',
            'url' => env('URL') . 'uploads/cropped-' . $filename
        ], 200);

    }


    private function sanitize($string, $force_lowercase = true, $anal = false)
    {
        $strip = array("~", "`", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "=", "+", "[", "{", "]",
            "}", "\\", "|", ";", ":", "\"", "'", "&#8216;", "&#8217;", "&#8220;", "&#8221;", "&#8211;", "&#8212;",
            "—", "–", ",", "<", ".", ">", "/", "?");
        $clean = trim(str_replace($strip, "", strip_tags($string)));
        $clean = preg_replace('/\s+/', "-", $clean);
        $clean = ($anal) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean ;

        return ($force_lowercase) ?
            (function_exists('mb_strtolower')) ?
                mb_strtolower($clean, 'UTF-8') :
                strtolower($clean) :
            $clean;
    }


    private function createUniqueFilename( $filename )
    {
        $upload_path = env('UPLOAD_PATH');
        $full_image_path = $upload_path . $filename . '.jpg';

        if ( File::exists( $full_image_path ) )
        {
            // Generate token for image
            $image_token = substr(sha1(mt_rand()), 0, 5);
            return $filename . '-' . $image_token;
        }

        return $filename;
    }
}

On success backend will return url of cropped image, and Croppic will display it.

As you could see, I am not deleting images that are not used from server or doing any kind of cleaning. You can code that part in number of ways, for details checkout ImageRepository of my other tutorial related to Dropzone and Laravel.

Don't forget to give proper permissions to public/uploads folder.

Online computer science courses to jumpstart your future.
WP Engine Managed WordPress Hosting

Trending

Newsletter

Subscribe to our newsletter for good news, sent out every month.

Tags