Editing Images Before Uploading in Vue JS Project
Tags: vue, javascript, vuetify, nuxt
Published: 2020-12-26 14:30:23

Hello Everyone, hope you are doing well and staying safe,


Today I will be sharing the small coding snippet that I created today to allow the users to edit/crop/rotate images before uploading them to the website. 

I am using Vuetify with Material Icons in my project and assume many of you use that as well as it is the one that's most popular.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/35lt0cbpbrxdwqv6510b.jpg)


The most interesting point in this tutorial is, Edited image will be simply HTML File Object, unlike other solutions available out there on the internet which returns bash string or blob, etc, where you have to modify your backend logic. 

so in our case, it would be fully compatible with the existing backend API uploading logic as it returns Default File Object.


First Install the dependency required to edit the image.

## Installation
```npm install --save vue-cropperjs```


Secondly, create a component named AvatarUpload in the components folder or somewhere else where you feel comfortable.

In my case, its located at /components/Utils/AvatarUpload in my nuxtjs based project
```javascript

<template>
  <div>
    <div class="content">
      <v-dialog v-model="isAvatarUploadDialogOpen" persistent max-width="600">
        <v-card>
          <v-card-title class="headline"></v-card-title>

          <v-card-text>
            <v-row no-gutters>
              <v-col class="">
                <section class="cropper-area">
                  <div class="img-cropper">
                    <vue-cropper
                      ref="cropper"
                      :initial-aspect-ratio="1"
                      :src="imgSrc"
                      preview=".preview"
                    />
                  </div>
                  <div class="actions mt-2">
                    <v-btn class="ma-1" small dark color="grey darken-3" @click.prevent="zoom(0.2)">
                      <v-icon> mdi-magnify-plus-outline</v-icon>
                    </v-btn>
                    <v-btn class="ma-1" small dark color="grey darken-3" @click.prevent="zoom(-0.2)">
                      <v-icon> mdi-magnify-minus-outline</v-icon>
                    </v-btn>
                    <v-btn class="ma-1" small dark color="grey darken-3" @click.prevent="rotate(90)">
                      <v-icon href="#">
                        mdi-axis-x-rotate-clockwise
                      </v-icon>
                    </v-btn>
                    <v-btn class="ma-1" small dark color="grey darken-3" @click.prevent="rotate(-90)">
                      <v-icon>
                        mdi-axis-x-rotate-counterclockwise
                      </v-icon>
                    </v-btn>
                    <v-btn class="ma-1" small dark color="grey darken-3" @click.prevent="move(10, 0)">
                      <v-icon>
                        mdi-arrow-left
                      </v-icon>
                    </v-btn>
                    <v-btn class="ma-1" small dark color="grey darken-3" @click.prevent="move(-10, 0)">
                      <v-icon>
                        mdi-arrow-right
                      </v-icon>
                    </v-btn>
                    <v-btn class="ma-1" small dark color="grey darken-3" @click.prevent="move(0, 10)">
                      <v-icon>
                        mdi-arrow-up
                      </v-icon>
                    </v-btn>
                    <v-btn class="ma-1" small dark color="grey darken-3" @click.prevent="move(0, -10)">
                      <v-icon>
                        mdi-arrow-down
                      </v-icon>
                    </v-btn>


                    <!--              <v-btn class="ma-1" small dark color="grey darken-3"  @click.prevent="cropImage">-->
                    <!--                <v-icon > mdi-crop</v-icon>-->
                    <!--              </v-btn>-->
                    <v-btn class="ma-1" small dark color="grey darken-3">
                      <v-icon @click.prevent="reset"> mdi-lock-reset</v-icon>
                    </v-btn>
                  </div>
                </section>
                <div class="text-right">
                  <v-btn small dark color="grey darken-3 mt-3" @click.prevent="submitImage">
                    <v-icon> mdi-send</v-icon>
                    Submit
                  </v-btn>
                </div>


                <!--            <div class="mt-4">-->
                <!--              <v-btn small dark color="grey darken-3" @click.prevent="showFileChooser">-->
                <!--                <v-icon>-->
                <!--                  mdi-upload-->
                <!--                </v-icon>-->
                <!--                Upload Image-->
                <!--              </v-btn>-->
                <!--            </div>-->

                <!-- <textarea v-model="data" /> -->

              </v-col>

              <!--        <v-col cols="12" sm="6">-->
              <!--          <section class="preview-area">-->
              <!--            &lt;!&ndash;            <p>Preview</p>&ndash;&gt;-->
              <!--            <div class="preview"/>-->
              <!--            <div class="cropped-image">-->
              <!--              <img v-if="cropImg" :src="cropImg" alt="Cropped Image"/>-->
              <!--              <div v-else class="crop-placeholder"/>-->
              <!--            </div>-->
              <!--          </section>-->
              <!--        </v-col>-->
            </v-row>

          </v-card-text>
        </v-card>
      </v-dialog>

    </div>
  </div>
</template>

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

export default {
  components: {
    VueCropper,
  },
  props: {
    // eslint-disable-next-line vue/require-prop-types
    image: {
      required: true
    },

  },
  data() {
    return {
      imgSrc: "/assets/images/berserk.jpg",
      cropImg: "",
      data: null,
      filename: "",
      mimeType: "",
      isAvatarUploadDialogOpen: false,
    };
  },
  watch: {
    image(value) {
      this.initialImageSetUp(value)
    }
  },
  mounted() {
    this.initialImageSetUp(this.image)
  },
  methods: {
    initialImageSetUp(value) {
      const self = this

      if (!value) {
        this.cropImg = ""
        this.$emit('close', true)
        return;
      }
      this.isAvatarUploadDialogOpen = true
      self.filename = value.name
      self.mimeType = value.type
      self.setImage(value)
    },
async dataURLToFile(imageString, filename, mimeType) {

    const res = await fetch(imageString);
    const blob = await res.blob();
    return new File([blob], filename, { type: mimeType });
  },
    async submitImage() {
      await this.cropImage();
      const imageFileResponse = await this.dataURLToFile(this.cropImg, this.filename, this.mimeType)
      this.$emit('done', imageFileResponse)
      this.isAvatarUploadDialogOpen = false

    },
    cropImage() {
      // get image data for post processing, e.g. upload or setting image src
      this.cropImg = this.$refs.cropper.getCroppedCanvas().toDataURL();
    },
    // flipX() {
    //   const dom = this.$refs.flipX;
    //   let scale = dom.getAttribute("data-scale");
    //   scale = scale ? -scale : -1;
    //   this.$refs.cropper.scaleX(scale);
    //   dom.setAttribute("data-scale", scale);
    // },
    // flipY() {
    //   const dom = this.$refs.flipY;
    //   let scale = dom.getAttribute("data-scale");
    //   scale = scale ? -scale : -1;
    //   this.$refs.cropper.scaleY(scale);
    //   dom.setAttribute("data-scale", scale);
    // },
    getCropBoxData() {
      this.data = JSON.stringify(this.$refs.cropper.getCropBoxData(), null, 4);
    },
    getData() {
      this.data = JSON.stringify(this.$refs.cropper.getData(), null, 4);
    },
    move(offsetX, offsetY) {
      this.$refs.cropper.move(offsetX, offsetY);
    },
    reset() {
      this.$refs.cropper.reset();
    },
    rotate(deg) {
      this.$refs.cropper.rotate(deg);
    },
    setCropBoxData() {
      if (!this.data) return;

      this.$refs.cropper.setCropBoxData(JSON.parse(this.data));
    },
    setData() {
      if (!this.data) return;

      this.$refs.cropper.setData(JSON.parse(this.data));
    },
    setImage(file) {

      if (!file.type.includes("image/")) {
        alert("Please select an image file");
        return;
      }

      if (typeof FileReader === "function") {
        const reader = new FileReader();

        reader.onload = (event) => {
          this.imgSrc = 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");
      }
    },
    showFileChooser() {
      this.$refs.input.click();
    },
    zoom(percent) {
      this.$refs.cropper.relativeZoom(percent);
    },
  },
};
</script>

<style>
input[type="file"] {
  display: none;
}

.cropped-image {
  padding: 0 .8rem;
}

.img-cropper {
  max-height: 400px;
  overflow: hidden;
}

</style>
```

# Using AvatarUpload Component

In any component where you want to implement the ImageEditing Features. use it like this

```javascript

<template>
  <div>
    <v-container>
      <v-file-input
        v-model="avatarImage"
        label="File input"
      ></v-file-input>
      <AvatarUpload :image="avatarImage" @done="(image) => form.image=image"/>
    </v-container>
  </div>
</template>

<script>
import AvatarUpload from "@/components/Utils/AvatarUpload";

export default {
  components: {AvatarUpload},
  data() {
    return {
      avatarImage: null,
      form: {
        image: null
      }
    }
  },
}
</script>

<style scoped></style>
```