← Home

Image Crop and Upload with Laravel & Vue.js

September 10, 2019

Recently, I worked on a project based on Laravel and Vue.js where I had to set up a manual image cropping tool with server upload. I then decided to share it in this tutorial.

Image crop

Getting started

Assuming you have Laravel already installed on your server or your local machine, otherwise, follow the installation guide from the Laravel official website.

For this tutorial, minimal required versions are Laravel 5.4 and Vue.js 2.

Creating the cropper component

Laravel provides a great support for Vue.js. So, we will build our Vue components directly inside the Laravel’s assets folder.

All components are located in ressource/js/components, open that folder and create a new component CropUploadComponent.vue.

<template>
  <div>
    Image crop and upload
  </div>
</template>

<script>
  export default {
    name: "CropUploadComponent",
  }
</script>

Then, register that component in ressources/js/app.js before const app by adding the following line

Vue.component(
  "crop-upload",
  require("./components/CropUploadComponent.vue").default
)

Run the command below from your project root directory to install the necessary node modules.

$ npm install

Next, start compiling assets using the following command.

$ npm run dev

You can also run the following command to compile the assets as you write new code or modify the existing code.

$ npm run watch

Open your laravel view file located on resources/views, in this case we are using default home view welcome.blade.php, Then import assets and the cropper component as follow.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <title>Image crop upload</title>

    <link href="{{ mix('css/app.css') }}" type="text/css" rel="stylesheet" />
  </head>
  <body class="bg-light">
    <div class="w-75 mx-auto my-5">
      <h2 class="mb-3">Image crop upload</h2>
      <div id="app">
        <crop-upload></crop-upload>
      </div>
    </div>

    <script src="{{ mix('js/app.js') }}" type="text/javascript"></script>
  </body>
</html>

Let’s start building our component by creating the upload button, image preview and preview function

<template>
  <div>
    <input type="file" name="image" accept="image/*" @change="setImage" />
    <img :src="imageSrc" style="width: 100px;" />
  </div>
</template>

<script>
  export default {
    data: function () {
      return {
        imageSrc: "",
      }
    },
    methods: {
      setImage: function (e) {
        const file = e.target.files[0]
        if (!file.type.includes("image/")) {
          alert("Please select an image file")
          return
        }
        if (typeof FileReader === "function") {
          const reader = new FileReader()
          reader.onload = event => {
            this.imageSrc = event.target.result

            // Rebuild cropperjs with the updated source
            this.$refs.cropper.replace(event.target.result)
          }
          reader.readAsDataURL(file)
        } else {
          alert("Sorry, FileReader API not supported")
        }
      },
    },
  }
</script>

The code above will create image previewer

Preview component

Image cropper

There is plenty of cropping packages for Vue.js, I tried many of them and I found that awesome package vuecropperjs

Run the following command from your project root directory to install the vuecropperjs

$ npm install --save vue-cropperjs

Then import the cropper in CropUploadComponent.vue

import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';

export default {
    components: {
        VueCropper
    },
    ...
}

Include the cropper component in the template part

<vue-cropper
  class="mr-2 w-50"
  ref="cropper"
  :guides="true"
  :src="imageSrc"
></vue-cropper>

If you want to add extra features, discover all cropperjs options via this link,

Full code CropUploadComponent.vue

<template>
  <div class="p-3 bg-white shadow rounded-lg">
    <input type="file" name="image" accept="image/*" @change="setImage" />

    <!-- Image previewer -->
    <img :src="imageSrc" width="100" />

    <!-- Cropper container -->
    <div
      v-if="this.imageSrc"
      class="my-3 d-flex align-items-center justify-content-center mx-auto"
    >
      <vue-cropper
        class="mr-2 w-50"
        ref="cropper"
        :guides="true"
        :src="imageSrc"
      ></vue-cropper>

      <!-- Cropped image previewer -->
      <img class="ml-2 w-50 bg-light" :src="croppedImageSrc" />
    </div>
    <button v-if="this.imageSrc" @click="cropImage">Crop</button>
    <button v-if="this.croppedImageSrc" @click="uploadImage">Upload</button>
  </div>
</template>

<script>
  import VueCropper from "vue-cropperjs"
  import "cropperjs/dist/cropper.css"

  export default {
    components: {
      VueCropper,
    },
    data: function () {
      return {
        imageSrc: "",
        croppedImageSrc: "",
      }
    },
    methods: {
      setImage: function (e) {
        const file = e.target.files[0]
        if (!file.type.includes("image/")) {
          alert("Please select an image file")
          return
        }
        if (typeof FileReader === "function") {
          const reader = new FileReader()
          reader.onload = event => {
            this.imageSrc = event.target.result

            // Rebuild cropperjs with the updated source
            this.$refs.cropper.replace(event.target.result)
          }
          reader.readAsDataURL(file)
        } else {
          alert("Sorry, FileReader API not supported")
        }
      },
      cropImage() {
        // Get image data for post processing, e.g. upload or setting image src
        this.croppedImageSrc = this.$refs.cropper.getCroppedCanvas().toDataURL()
      },
      uploadImage() {
        this.$refs.cropper.getCroppedCanvas().toBlob(function (blob) {
          let formData = new FormData()

          // Add name for our image
          formData.append("name", "image-name-" + new Date().getTime())

          // Append image file
          formData.append("file", blob)

          axios
            .post("/api/store", formData)
            .then(response => {
              console.log(response.data)
            })
            .catch(function (error) {
              console.log(error)
            })
        })
      },
    },
  }
</script>

Image upload

Let’s move to the backend and create our upload system

First, add the store action in routes/api.php for our previous axios call

Route::post('store', 'ImageController@store');

We now need to implement this action in the controller. Go to app/http/controllers and create a new controller ImageController.php.

<?php
namespace App\Http\Controllers;

class ImageController extends Controller
{
    public function store()
    {
        return response()->json(['success' => 'It works!']);
    }
}

Next, let’s install the Intervention image library in our Laravel project. Run the command below from your project root directory.

composer require intervention/image

After installing the library, open config/app.php file and add the following lines. Add the service providers for this package in the providers array.

Intervention\Image\ImageServiceProvider::class

Add the facade to the aliases array.

'Image' => Intervention\Image\Facades\Image::class

Full file imageController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Image;

class ImageController extends Controller
{
    public function store(Request $request)
    {
        if ($request->hasFile('file')) {

            $image = $request->file;
            $name = $request->name.'.jpg';
            $path = 'public/images/' . $name;

            $img = Image::make($image);

            Storage::disk('local')->put($path, $img->encode());

            $url = asset('storage/images/' . $name);

            return response()->json(['url' => $url]);
        }

        return response()->json(['error' => 'No file']);
    }
}

After processing the upload, you will find your image in the storage folder as shown in the following image

Uploaded image folder

Conclusion

In this tutorial, we’ve created a cropping and uploading system using Laravel and Vue.js. This system is not totally complete and lacks some features either on the frontend as uploading progress or on the backend as image type validation, that we’ll talk about in future blog posts.

Finally if you have any questions, do not hesitate to contact me.


Chafik Gharbi Full-stack web and mobile app developer with JavaScript.