Image Cropper in Vue.js

  • Hello guys, In this article we will learn how to crop particular image and set cropped image in profile pic using vue.js/CLI.
  • Create new Vue.js project using below command:
vue create youprojectname
  • Install bootstrap-vue using below command:
vue add bootstrap-vue
  • Here we use vue.js-clipper, it is an plugin that provides lots of functionality for managing image cropping service.
  • Clipper basic allows the use of a clipper range (e.g) to rotate and scale programmatically. Clipper fixed does not and it is as important as it is in Clipper Basic functionalities.
  • Install Vue.js Clipper using below command:
npm install vuejs-clipper --save
  • Vue.js-clipper is based on Vue-rx.
  • Vue-rx is required as a peer dependency.
  • When bundling via webpack, dist/vue-rx.esm.js is used by default. It imports the minimal amount of Rx operators and ensures small bundle sizes.
  • You can clip your images (local uploaded images or images served on your site), but you cannot clip a cross-origin image unless the image server sets the CORS headers.
  • Components’ input is an image URL, output is a canvas element, they only help you clip images to canvas, you need to handle other things like transform file input to image URL or transform output canvas to image by yourself.
  • Install peer dependencies if you haven’t using below commad:
npm install vue-rx rxjs --save
  • After Successfully Installation of vue.js-clipper and vue-rx we need to import both in main.js
  • Your main.js file looks like below:
import '@babel/polyfill'
import 'mutationobserver-shim'
import Vue from 'vue'
import './plugins/bootstrap-vue'
import App from './App.vue'
import VueRx from 'vue-rx'
import VuejsClipper from "vuejs-clipper/dist/vuejs-clipper.umd";
import "vuejs-clipper/dist/vuejs-clipper.css";
Vue.use(VueRx);
Vue.use(VuejsClipper);
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
  • Lets create upload.vue component
  • Add the following code in upload.vue
<template>
  <div>
    <div
      :class="{
        'img-photo': true,
        'img-photo-w160': isLandscape,
        'img-photo-h120': isPortrait,
      }"
    >
      <span v-if="value !== ''">
        <img
          id="image-upload-view"
          :key="value"
          :src="value"
          class="img-view"
          width="40"
          alt
        />
      </span>
      <span v-else>
        <img
          id="image-upload-view"
          src="./camera-icon-black.svg"
          width="40"
          alt
        />
      </span>
    </div>
    <div class="upload-btn-wrapper">
        <button
        type="button"
        class="btn"
        @click="onUploadPictureClick"
        :disabled="disableButton"
      >{{buttonName}}</button>
      <input
        :id="id"
        type="file"
        tabindex="1"
        :disabled="disableButton"
        @input="onimageUpload($event)"
        @change="onimageUpload($event)"
        @click="onUploadPictureClick"
      />
      
        
    </div>
    <p
      v-show="fileUploadErrorMessage || fileError || fileSizeErrorMessage"
      :class="{ 'error-msg': true }"
    >
      <span v-show="fileUploadErrorMessage">{{ imageUploadErrorMessage }}</span>
      <span v-show="fileSizeErrorMessage">{{ imageSizeErrorMessage }}</span>
      <span v-show="fileError">{{ invalidImageErrorMessage }}</span>
    </p>
    <div>
      <b-modal
        v-model="isShowCropModal"
        id="image-crop"
        no-close-on-esc
        no-close-on-backdrop
      >
        <template v-slot:modal-header>
          <span aria-hidden="true" class="close pointer" @click="onCancelCrop"
            >×</span
          >
        </template>
        <clipper-fixed
          ref="clipper"
          class="my-clipper"
          :src="value"
          :round="isRoundCrop"
          :area="40"
        />
        <template v-slot:modal-footer>
          <div class="popup-btn">
            <b-button @click="onCrop">Crop</b-button>
          </div>
          <div class="popup-btn">
            <b-button @click="onCancelCrop">Cancel</b-button>
          </div>
        </template>
      </b-modal>
    </div>
  </div>
</template>

<script>
import Vue from "vue";
import * as utils from "@/components/utils.js";
import VuejsClipper from "vuejs-clipper";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { BootstrapVue } from "bootstrap-vue";

Vue.use(BootstrapVue);

Vue.use(VuejsClipper, {
  components: {
    clipperBasic: true,
    clipperPreview: true,
    clipperFixed: true,
  },
});

export default {
  data() {
    return {
      isShowCropModal: false,
      tempImageUrl: "",
      fileUploadErrorMessage: false,
      fileSizeErrorMessage: false,
      fileError: false,
    };
  },
  components: {},
  props: {
    isCrop: {
      type: Boolean,
      default: true,
    },
    acceptedFiles: {
      type: Array,
      default: function() {
        return ["image", "image/jpeg", "image/jpg", "image/png", "png"];
      },
    },
    value: {
      type: String,
      default: "",
    },
    id: {
      type: String,
      default: "image-upload",
    },
    disableButton: {
      type: Boolean,
      default: false,
    },
    buttonName: {
      type: String,
      default: "Upload image",
    },
    imagesize: {
      type: Number,
      default: 5000000,
    },
    isLandscape: {
      type: Boolean,
      default: false,
    },
    isPortrait: {
      type: Boolean,
      default: false,
    },
    imageUploadErrorMessage: {
      type: String,
      default: "Only jpg, jpeg and png are allowed.",
    },
    imageSizeErrorMessage: {
      type: String,
      default: "Failed to upload an image. The image maximum size is 5MB.",
    },
    invalidImageErrorMessage: {
      type: String,
      default: "The uploaded image is not valid.",
    },
    isRoundCrop: {
      type: Boolean,
      default: true,
    },
  },
  methods: {
    onUploadPictureClick(event) {
      event.target.value = "";
    },

    onCrop() {
      const canvas = this.$refs.clipper.clip();
      this.value = canvas.toDataURL();
      this.isShowCropModal = false;
    },

    onCancelCrop() {
      this.value = this.tempImageUrl;
      this.isShowCropModal = false;
      this.tempImageUrl = "";
    },

    onimageUpload(e) {
      if (this.value !== "") {
        this.tempImageUrl = this.value;
      }
      let width;
      let height;
      const file = e.target.files[0];
      if (file !== undefined) {
        if (file.type !== undefined && this.acceptedFiles.includes(file.type)) {
          const reader = new FileReader();
          reader.readAsDataURL(file);
          reader.onloadend = () => {
            const image = new Image();
            image.src = reader.result;
            image.onload = () => {
              width = image.width;
              height = image.height;
              const block = image.src.split(";");
              const contentType = block[0].split(":")[1];
              const realData = block[1].split(",")[1];
              const blob = utils.b64toBlob(realData, contentType);
              const imageUrl = window.URL.createObjectURL(blob);
              this.onValidImage(file, imageUrl, this.id, width, height, e);
            };
          };
        } else {
          this.onValidImage(file, undefined, this.id, undefined, undefined, e);
        }
      }
      e.target.value = "";
    },

    onValidImage(file, url, id, width, height, e) {
      if (id === this.id) {
        this.fileSizeErrorMessage = false;
        this.fileUploadErrorMessage = false;
        this.fileError = false;
        if (this.acceptedFiles.includes(file.type)) {
          if (this.imagesize > file.size) {
            if (this.isLandscape) {
              if (width > height) {
                this.value = url;
                if (this.isCrop) {
                  this.isShowCropModal = true;
                }
              } else {
                this.fileError = true;
              }
            } else if (this.isPortrait) {
              if (width < height) {
                this.value = url;
                if (this.isCrop) {
                  this.isShowCropModal = true;
                }
              } else {
                this.fileError = true;
              }
            } else {
              this.value = url;
              if (this.isCrop) {
                this.isShowCropModal = true;
              }
            }
          } else {
            this.fileSizeErrorMessage = true;
          }
        } else {
          this.fileUploadErrorMessage = true;
        }
      }
      e.target.value = "";
    },
  },
};
</script>

<style scoped>
@import "./style.css";
</style>
  • Create utils.js file and add the following code in it
function b64toBlob(b64Data, contentType, sliceSize) {
    contentType = contentType || ''
    sliceSize = sliceSize || 512
    const byteCharacters = atob(b64Data)
    const byteArrays = []
    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
      const slice = byteCharacters.slice(offset, offset + sliceSize)
  
      const byteNumbers = new Array(slice.length)
      for (let i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i)
      }
      const byteArray = new Uint8Array(byteNumbers)
  
      byteArrays.push(byteArray)
    }
    const blob = new Blob(byteArrays, {
      type: contentType,
    })
    return blob
  }

  export {
    b64toBlob
  }
  • Create style.css and add following code in it
.upload-btn-wrapper {
  position: relative;
  display: inline-block;
  overflow: hidden;
  vertical-align: middle;
}
  
  .upload-btn-wrapper .btn {
    padding: 5px 35px;
    font-size: 15px;
    color: #000;
    background-color: white;
    border-radius: 5px;
    border: 2px solid;
    color: black;
    background-color: #c9c9d1;
  }
  
  .upload-btn-wrapper input[type="file"] {
    position: absolute;
    top: 0;
    left: 0;
    font-size: 100px;
    opacity: 0;
  }
  
  .img-photo {
    display: inline-block;
    width: 85px;
    height: 85px;
    margin-right: 30px;
    line-height: 80px;
    color: #ddd;
    text-align: center;
    vertical-align: middle;
    background: #fff;
    border-radius: 50px;
    border: 2px solid;
  }
  
  .img-photo .img-view {
    width: 85px !important;
    height: 85px !important;
    border-radius: 0 !important;
    border: 2px solid;
  }
  .error-msg {
    display: inline-block;
    width: 100%;
    padding-top: 9px;
    padding-top: 8px;
    font-size: 18px;
    font-weight: 500;
    color: #b70000;
  }
  .img-photo-w160 {
    width: 160px;
    border-radius: 10px;
    border: 2px solid;
  }
  
  .img-photo-w160 .img-view {
    width: 160px !important;
    border-radius: 10px;
    border: 2px solid;
  }
  
  .img-photo-h120 {
    height: 120px;
    border-radius: 10px;
    border: 2px solid;
  }
  
  .img-photo-h120 img {
    margin-top: 40px;
  }
  
  .img-photo-h120 .img-view {
    height: 120px !important;
    margin-top: 0;
    border-radius: 10px;
    border: 2px solid;
  }
  • Finally your App.vue file looks like below
<template>
  <div id="app">
   <upload/>
  </div>
</template>

<script>
import upload from '@/components/upload.vue'

export default {
  name: 'App',
  components: {
    upload
  }
}
</script>

<style lang="scss">
@import "~@/assets/scss/vendors/bootstrap-vue/index";

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
  • Now run you application and you will get following output:

Submit a Comment

Your email address will not be published. Required fields are marked *

Subscribe

Select Categories