Upload and Resize an Image with Laravel Vue and ImageMagick

 ← Dev Articles
👍 0
👎 0

In this example I use the Laravel and Vue frameworks to implement a SPA tool to upload images and display a list of images that have been uploaded.  The application also demonstrates image resizing with the amazing PHP ImageMagick library.  So there are a few prerequisits to get started however I won't cover that in this tutorial.  I've added some links to get you through the prerequisits:

  1. Install imagemagick and the pecl imagemagick package. Configure the extension in php.
  2. Install and set-up Laravel to use the Vue 2 Options API with Webpack compiler.
  3. Create required Controllers, Blade Views, routes(web.php, api.php, routes.js) and Vue components

Assuming you have an initial setup you will need to generate an API controller to handle requests made from the Vue components.  You can generate the controller with php artisan:

php artisan make:controller Api/ImageController

Next, we need to specify a directory to store these images.  For this tutorial we will upload our images to /public/images.  So notice in the ImageController the method public_path('images/'.$imageName) is used to specify this location.

 

/app/Http/Controllers/Api/ImageController.php

We need two methods to handle the functionality for this application.  An index method for GET requests to retrieve a list of all images.  And an upload method to handle a POST request to upload an image.

 

<?php

namespace App\Http\Controllers;

 

use Illuminate\Http\Request;

use Illuminate\Support\Facades\File;

use Imagick;

 

class ImageController extends Controller

{

   /**

    * index

    */

   public function index()

   {

       $fileNames = [];

       $imagesPath = public_path('images/');

       $files = File::files($imagesPath);

       foreach ($files as $key=>$file) {

           $fileNames[] = [

               'file'=>$file->getFilename(),

               'path'=>$file->getPathname(),

               'size'=>$file->getSize(),

               'ext'=>$file->getExtension()

           ];

       }

       return response()->json($fileNames);

   }

 

 

   /**

    * upload

    */

   public function upload(Request $request)

   {

       $request->validate([

           'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',

       ]);

 

       // Create a new Imagick object

       $imagick = new Imagick();

 

       // Read the uploaded image

       $imagick->readImage($request->file('image')->getPathname());

 

       // Resample the image (change the resolution)

       $imagick->resizeImage(100, 100, Imagick::FILTER_LANCZOS, 1);

 

       // Save the image

       $imageName = time() . '.' . $request->file('image')->getClientOriginalExtension();

       $imagick->writeImage(public_path('images/' . $imageName));

 

       // Clear the Imagick object

       $imagick->clear();

       $imagick->destroy();

 

       $fileNames = [];

       $imagesPath = public_path('images/');

       $files = File::files($imagesPath);

       foreach ($files as $file) {

           $fileNames[] = [

               'file'=>$file->getFilename(),

               'path'=>$file->getPathname(),

               'bytes'=>$file->getSize(),

               'ext'=>$file->getExtension()

           ];

       }

      

       return response()->json($fileNames);

   }
}

 

/resources/js/Image/Upload.vue

The Upload.vue component will display an image upload form and a script to interact with the form actions.  Once the user selects an image, they will be able to preview the image before upload.  Notice the API request made with Axios POST to /api/images.  These routes are specified in /routes/api.php

<template>

   <div class="container">

       <div class="row justify-content-center">

           <div class="col-md-8">

               Upload Image Form

                   <div class="row m-1 g-1">

                       <div class="form-group col-md-12">

                           <input class="form-control" :class="[{'text-danger': 'image' in errors && errors.image.length > 0}]" type="file" @change="onFileChange" name="image" id="image"  accept="image/*" >

                       </div>

                       <div v-if="'image' in errors" class="text-danger">                

                           <span v-for="(error, index) in errors['image']" :key="'type-'+index">

                               {{error}}

                           </span>

                       </div>

                   </div>

                   <div v-if="preview" class="mt-3">

                       <img :src="preview" alt="preview" width="100" height="100" />

                   </div>

                   <div class="row m-1 g-1">

                       <div class="form-group col-12">

                           <button type="button" @click.prevent="cancelUpload" class="btn btn-sm btn-fw btn-danger float-end mx-2">

                               Cancel

                           </button>

                           <button class="btn btn-sm btn-fw btn-primary float-end" type="submit" @click.prevent="uploadImage">Upload Image</button>

                       </div>

                   </div>

           </div>

       </div>

   </div>

</template>

<script>
export default {

 

   data() {

       return {

           image: null,

           preview: null,

           user: {},

           errors:{

               image: []

           },

       };

   },

 

   methods: {

       cancelUpload(){

           let redirectTo = this.$route.query.redirect || { name: 'admin-images' };

           this.$router.push(redirectTo);

       },

       async uploadImage(){

           if(this.image)

           {

               let fd = new FormData();

               fd.append('image', this.image);

               try {

                   let resp = (await axios.post('/api/images', fd)).data.data;

                   let redirectTo = this.$route.query.redirect || { name: 'admin-images' };

                   this.$router.push(redirectTo);

               } catch(error) {

                   this.errors = error;

               }

           }

           else {

               this.errors.image.push(`An image file is required for upload.`);

           }

          

       },

       onFileChange(e) {

           let file = e.target.files[0];

           if (file) {

               this.image = file;

               this.preview = URL.createObjectURL(file);

           }

       }

   },

   created() {

       console.log('Image/Upload COMPONENT CREATED.');       

   }
}
</script>

 

/resources/admin/js/Image/Index.vue

The Index.vue component will display a list of images that currently exist in the /public/images directory.  Also notice the API GET request made to /api/images.  

<template>

   <div class="container">

       <div class="row justify-content-center">

           <div class="col-md-10">

               <router-link

                   class="btn btn-primary mb-2 float-right"

                   :to="{ name: 'image-upload', query: {redirect: $route.fullPath} }">

                Image Upload

               </router-link>

 

               <div class="mb-4 row" v-for="(image, index) in images" :key="index">

                <img

                       :src="`/images/${image.file}`"

                       class="img-thumbnail "

                       width="100"

                     />

               </div>

           </div>

       </div>

   </div>

</template>

<script>

export default {

   data() {

       return {

           images: [],

           user: {}

       };

   },

   methods: {

   },

   async created() {

       let response = await axios.get("/api/images");

       this.images = response.data.length ? response.data : [];

   }

};

</script>

 

/resources/js/routes.js

In order to see the Image/Index.vue and Image/Upload.vue components we need to specify a route for each in routes.js.  Because the path /images and /image/upload routes are handled via Vue we need to use a <router-link> inside of another Vue component order to navigate to /images and /image/upload.  

<router-link class="nav-link active" :to="{name: 'image-index'}">Images</router-link>
<router-link class="nav-link active" :to="{name: 'image-upload'}">Upload</router-link>

Then configure the routes in /resources/js/routes.js like below:

 

 import ImageIndex from "./Image/Index";

 import ImageUpload from "./Image/Upload";

 

 const routes = [

       // add these 2 new routes

{

    path: "/images",

    component: ImageIndex,

    name: "image-index",

},

{

    path: "/image/upload",

    component: ImageUpload,

    name: "image-upload",

}

 ];

 

 

/routes/api.php

Finally we create API routes to upload and retrieve images.  Notice both routes both go to /images and either Route::get or Route::post will be used to identify them properly in a request.

 

// GET request to ‘/api/images’

Route::get('/images', [App\Http\Controllers\Api\ImageController::class, 'index']);

 

// POST Request to ‘/api/images’
Route::post('/images', [App\Http\Controllers\Api\ImageController::class, 'upload']);