Recently I have been receiving request to provide a Laravel 5 module that will help to upload profile photo that is similar to something like provided by Facebook, Twitter or Linkedin where users can upload any sizes of photo and crop the portion of it according to how they need to be displayed.
It works in similar way as Twitter, Facebook or Linkedin profile photo upload. First use can upload the photo and then they are capable to drag,resize,zoom in or out and crop it and save. Could not be any simpler than this!
The best part is that you set fixed size of 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 if it unnecessary.
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 Laravel Image Intervention package, for server side image processing. If you want to install Laravel Image Intervention package please followup with this Tutorial
http://image.intervention.io/getting_started/installation
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
I donot dwell on the designing of html, I just copied the html code from Croppic website it self to depict for you.
<!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 define 3 routes function
- One for profile page where photo upload button is displayed, this is usually the member profile update page.
- Second route function to validate and upload the selected photo to server and return the URL to cropper for further processing.
- Third route function to save the cropped image.
<?php Route::get('/', 'CropController@getHome'); Route::post('upload', 'CropController@postUpload'); Route::post('crop', 'CropController@postCrop');
Solution for CSRF Verification while using Croppic Library
If you face CSRF token error with Croppic Library in Laravel, either you can disable CSRF verification by adding the second and third routes to except array in VerifyCsrfToken Middleware or add X-CSRF-TOKEN
request header so it instructs to include token in all ajax request headers, as show by the following code respectively.
Disable the CSRF verification
<?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' ]; }
X-CSRF-TOKEN
note that the following code snippet must be included after jquery library been loaded.
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } });
Server Side Processing
Image Model and Migration
I am going to store the data in database table for further perusal.
<?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' ]; }
Its always best practice to keep your models in a separate folder 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 - Controller function
This method is called immediately after image is selected by the user.
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, Server-side code will send error response, and Croppic will throw alert dialog.
One note: default alert window looks pretty ugly, so I alway use SweetAlert. You can use your own method to display error message, to customize the error message, you can use the onError property in the croppic Library.
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("~", "`", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "=", "+", "[", "{", "]", "}", "\\", "|", ";", ":", "\"", "'", "‘", "’", "“", "”", "–", "—", "—", "–", ",", "<", ".", ">", "/", "?"); $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 and save, Controller function
After user clicks on crop button Croppic will send data to Server side for cropping image and save it. Actually Croppic is not doing any cropping :-) it only sends x/y coordinates of the image and width and height of cropping mask. We will have to code cropping logic in the server side code. Croppic project provides some basic php script for this, but I am using Image Intervention package 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("~", "`", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "=", "+", "[", "{", "]", "}", "\\", "|", ";", ":", "\"", "'", "‘", "’", "“", "”", "–", "—", "—", "–", ",", "<", ".", ">", "/", "?"); $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 successfully processed on server side code, it 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 though you are free to perform any action as your requirement.
Don't forget to give write permissions to public/uploads
folder.
Be the first one to write a response :(
{{ reply.member.name }} - {{ reply.created_at_human_readable }}