Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transform to image after cropping #32

Closed
theprobugmaker opened this issue Jan 4, 2020 · 24 comments
Closed

Transform to image after cropping #32

theprobugmaker opened this issue Jan 4, 2020 · 24 comments

Comments

@theprobugmaker
Copy link

theprobugmaker commented Jan 4, 2020

I implemented the cropping with validation and everything works fine on the front end, I also need to validate the backend as the image is going to be sent from an API. If I validate on the front end as the max image size as 5MB and the same on the back end, it won't work cause as the cropper works with base64 I will have a WAY bigger image on the backend:

image

Do you have any idea on what I can do to solve this? I'm currently converting the base64 to an image before sending with this code:

export default dataURI => {
    let byteString =
        dataURI.split(',')[0].indexOf('base64') >= 0
            ? atob(dataURI.split(',')[1])
            : unescape(dataURI.split(',')[1])

    const mimeString = dataURI
        .split(',')[0]
        .split(':')[1]
        .split(';')[0]

    let ia = new Uint8Array(byteString.length)

    for (let i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i)
    }

    return new Blob([ia], { type: mimeString })
}

This code will convert the base64 image from the cropper to a Blob so I can send using axios.

This way I can validate the image on the back end as well but the image sizes are way different.

@Norserium
Copy link
Collaborator

Norserium commented Jan 4, 2020

Hello, @zefexdeveloper!

I didn't understand your problem clearly. How do you check the size of an image at the backend and frontend?

Could you try to use canvas.toBlob(callback, 'image/jpeg') method to get the blob, instead of using canvas.toDataURL and your custom method?

@theprobugmaker
Copy link
Author

Hi @Norserium, the file size on the back end depends on what technology you are using, this project I'm using Laravel (PHP) and it's just a matter of calling getSize but as we are talking about validation here, the validation rule itself will check if the file size is bigger than what was specified.

If you select an image, let's say with 500kb and console.log the base64 on Chrome it will show you the file size.

This was a 334kb image selected, turned 3.9mb:
image

I will try this canvas.toBlob, didn't know it existed.

@Norserium
Copy link
Collaborator

Norserium commented Jan 4, 2020

It's better to use Blob.size property to check the size of sending image.

@theprobugmaker
Copy link
Author

@Norserium Yes, but it doesn't change the problem of having different file size on selecting and when sending. For example, the front end validation is when you select a file right? This way you can check if the file isn't bigger than 5mb, but when you crop, the file size is way bigger. The validation on the back end is when sending the image, so I can't put like 5mb max on the back end as well cause if the user selects an image on the front end that is let's say 4.5mb and crops, it will turn at least 15mb more or less so the backend won't allow it.

I will try this canvas.toBlob cause working with base64 is problematic cause of this, the image size is way bigger.

@Norserium
Copy link
Collaborator

Norserium commented Jan 4, 2020

What's the image type do you use for converting to base64? Default is image/png, so if you upload .jpg image result might be larger.

@theprobugmaker
Copy link
Author

theprobugmaker commented Jan 4, 2020

@Norserium I was using the code above, it's pretty much the same as canvas.toBlob as I was reading and canvas.toBlob doesn't work on Edge and doesn't have the quality argument in most browsers so it will be pretty much the same. (or worse with less support: https://caniuse.com/#search=toblob)

Do you have a project set up where you can call canvas.toBlob and check if the file size is bigger (or a lot bigger) than it should be?

@Norserium
Copy link
Collaborator

Norserium commented Jan 5, 2020

The conceptual problem consists in impossibility to preserve size of the image after drawing one at an canvas and getting one from that canvas.

I can advise two ways:

  1. Use image/jpeg during conversion (toBlob, toDataURL). You won't get the same size, but it should be pretty similar.
  2. Crop the image on your backend by sending full image and coordinates to it.

Do you have a project set up where you can call canvas.toBlob and check if the file size is bigger (or a lot bigger) than it should be?

I've made it now:
https://codesandbox.io/s/vue-avanced-cropper-comparing-file-sizes-1xl7r

@theprobugmaker
Copy link
Author

@Norserium The second alternative sounds good, this way I can validate the size both on the front and on the back end without the cropping difference.

I will be trying to second alternative and see if it works nice. Thank you very much.

@Norserium
Copy link
Collaborator

@zefexdeveloper, did you solve your problem?

@theprobugmaker
Copy link
Author

@Norserium Sorry the delay answering. I refactored the code in order to send the coordinates and crop on the backend but the libraries asks for integer instead of float when cropping so I had to round them. Is it possible to have an option to return the coordinates as integer?

@Norserium
Copy link
Collaborator

I will investigate this possibility. Maybe I should add the prop roundCoordinates, but it's the pretty straightforward solution.

@theprobugmaker
Copy link
Author

@Norserium Thank you very much, it will help me a lot, right now I have to round each one and it's pretty shitty solution. Most of the others croppers I saw they have the coordinates as integers, having both in float for precision and integer for compatibility would be great.

@theprobugmaker
Copy link
Author

Anything on this @Norserium ? Thank you

@Norserium
Copy link
Collaborator

Norserium commented Jan 13, 2020

I'm pretty busy on other projects, so I've postponed the implementing of this feature to this weekends. Sorry for the delay.

@theprobugmaker
Copy link
Author

@Norserium That is ok, wish you luck. Thank you very much.

@Norserium
Copy link
Collaborator

Please update to 0.16.0 version.

@theprobugmaker
Copy link
Author

@Norserium You rock, thank you for this. It's rounded by default now right?

@Norserium
Copy link
Collaborator

Norserium commented Jan 19, 2020

Yes, I thought out and decided that's pretty optimal solution. Nonetheless, there is workaround to disable the rounding coordinates, but I couldn't come out with any situation where it might be needed.

@theprobugmaker
Copy link
Author

@Norserium Just updated here and it saved me a few lines of code rounding numbers 😆 Thank you

@simioluwatomi
Copy link

@Norserium Sorry the delay answering. I refactored the code in order to send the coordinates and crop on the backend but the libraries asks for integer instead of float when cropping so I had to round them. Is it possible to have an option to return the coordinates as integer?

Please how did you do this? Care to share some code snippets? I'm also using Laravel and I'm at my wits end because I don't know how to get the file object. Please help out. Thank you

@Norserium
Copy link
Collaborator

Norserium commented Mar 6, 2020

Hello, @simioluwatomi!

I'm not @zefexdeveloper, so I can't share the code snippets, but this task should be pretty trivial. Do you want to download image on the backend or send the cropped image from the frontend to the backend?

@simioluwatomi
Copy link

Hello, @simioluwatomi!

I'm not @zefexdeveloper, so I can't share the code snippets, but this task should be pretty trivial. Do you want to download image on the backend or send the cropped image from the frontend to the backend?

I've actually figured this out. Thank you!

@powolnymarcel
Copy link

powolnymarcel commented May 24, 2020

@simioluwatomi
Usually when you figure things out, you share it for others..

@simioluwatomi
Copy link

@powolnymarcel

Here you go

Vue component

<template>
    <div>
        <cropper
            :src="image"
            :stencil-props="{ aspectRatio: 1 }"
            ref="cropper"
            class="border border-dashed w-100"
            style="min-height: 300px; max-height: 600px"
        ></cropper>

        <p class="mt-3 text-center text-danger">{{ errorMessages[0] }}</p>

        <div class="btn-options justify-content-around my-3">

            <ValidationProvider rules="required|ext:jpg,png,jpeg|size:1024" ref="provider">

                <b-button variant="outline-primary" @click="$refs.file.click()">

                    <input type="file"
                           ref="file"
                           v-show="false"
                           accept="image/png,image/jpeg,image/jpg"
                           @change="loadImage($event)">

                    Select image

                </b-button>

            </ValidationProvider>

            <b-button variant="secondary" @click="rotate" :disabled="invalid">Rotate</b-button>

            <b-button variant="primary" type="submit" @click="upload" :disabled="invalid">Upload</b-button>

        </div>

    </div>

</template>

<script>
    import {Cropper} from "vue-advanced-cropper";
    import iziToast from "izitoast";
    import {ValidationProvider, ValidationObserver, extend} from 'vee-validate';
    import {required, ext, size} from 'vee-validate/dist/rules';

    extend('required', {
        ...required,
        message: 'This field is required.'
    });

    extend('ext', {
        ...ext,
        message: 'The selected file must be an image.'
    });

    extend('size', {
        ...size,
        message: "The selected image should not be greater than 1 megabyte."
    });


    export default {
        name: "ImageUploadComponent",
        components: {
            Cropper,
            ValidationProvider,
            ValidationObserver
        },
        props: {
            'user': {
                type: Object,
                required: true
            },
        },
        data() {
            return {
                image: null,
                avatar: null,
                invalid: true,
                errorMessages: '',
            };
        },
        methods: {
            async loadImage(event) {
                const {errors, valid} = await this.$refs.provider.validate(event);

                if (errors) {
                    this.errorMessages = errors;
                }

                if (valid) {
                    // Reference to the DOM input element
                    let input = event.target;
                    // Ensure that you have a file before attempting to read it
                    if (input.files && input.files[0]) {
                        // create a new FileReader to read this image and convert to base64 format
                        let reader = new FileReader();
                        // Define a callback function to run, when FileReader finishes its job
                        reader.onload = (event) => {
                            // Note: arrow function used here, so that "this.imageData" refers to the imageData of Vue component
                            // Read image as base64 and set to imageData
                            this.image = event.target.result;
                        };
                        this.avatar = input.files[0];
                        // Start the reader job - read file as a data url (base64 format)
                        reader.readAsDataURL(this.avatar);
                    }
                    this.invalid = false;
                }
            },
            rotate() {
                let image = document.createElement("img");
                image.crossOrigin = "anonymous";
                image.src = this.image;
                image.onload = () => {
                    let canvas = document.createElement("canvas");
                    let ctx = canvas.getContext("2d");

                    if (image.width > image.height) {
                        canvas.width = image.height;
                        canvas.height = image.width;
                        ctx.translate(image.height, image.width / image.height);
                    } else {
                        canvas.height = image.width;
                        canvas.width = image.height;
                        ctx.translate(image.height, image.width / image.height);
                    }
                    ctx.rotate(Math.PI / 2);
                    ctx.drawImage(image, 0, 0);
                    this.image = canvas.toDataURL();
                };
            },
            upload(blob) {
                const {canvas} = this.$refs.cropper.getResult();
                canvas.toBlob(result => {
                    let formData = new FormData();
                    formData.append('avatar', result, this.avatar.name);
                    axios.post(`/${this.user.username}/avatar`, formData)
                        .then(function (response) {                            
                            // handle success response here
                        }.bind(this))
                        .catch(function (error) {
                            // handle error response here
                        }.bind(this));
                }, 'image/jpeg')
            },
        },
    };
</script>

Controller method

public function __invoke(Request $request, User $user)
    {
        $request->validate(['avatar' => ['required', 'image', 'max:1024']], [
            'max' => 'The uploaded image should not be greater than 1 megabyte.',
        ]);

        $path = $request->file('avatar')->storeAs(
            "{$user->id}",
            sha1($user->id).'.jpeg',
            'uploads'
        );

        $user->update(['avatar' => $path]);

        return response()->json($user);
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants