fengyuanchen / cropperjs

JavaScript image cropper.
https://fengyuanchen.github.io/cropperjs/
MIT License
13k stars 2.4k forks source link

add php script example to handle the crop server side #626

Closed lingtalfi closed 2 years ago

lingtalfi commented 4 years ago

Hi, I love your cropperjs tool. I want to use it in my php projects, so I had to find a php handler script. Here is the function I would use as a starting point, I thought it might be useful to point to it from your documentation (maybe in this section: https://github.com/fengyuanchen/cropperjs#getdatarounded):

<?php

/**
 * Takes the given src image, and crops it to the given destination.
 * It returns null, or an error message if there was an error.
 *
 * The source is a file path.
 * The destination is a file path.
 * The data is an array with the following properties:
 *
 * - x: the offset left of the cropped area
 * - y: the offset top of the cropped area
 * - width: the width of the cropped area
 * - height: the height of the cropped area
 * - rotate: the rotated degrees of the image
 * - scaleX: the scaling factor to apply on the abscissa of the image
 * - scaleY: the scaling factor to apply on the ordinate of the image
 *
 * Note: if you are using cropperjs, you can get the data array by calling the getData method (https://github.com/fengyuanchen/cropperjs#getdatarounded).
 *
 *
 *
 *
 * @param string $src
 * @param string $dst
 * @param array $data
 * @return string|null
 */
function crop(string $src, string $dst, array $data): ?string
{
    $error = null;
    $src_type = exif_imagetype($src);

    $src_img = null;
    switch ($src_type) {
        case IMAGETYPE_GIF:
            $src_img = imagecreatefromgif($src);
            break;

        case IMAGETYPE_JPEG:
            $src_img = imagecreatefromjpeg($src);
            break;

        case IMAGETYPE_PNG:
            $src_img = imagecreatefrompng($src);
            break;
    }

    if (null !== $src_img) {

        $size = getimagesize($src);
        $size_w = $size[0]; // natural width
        $size_h = $size[1]; // natural height

        $src_img_w = $size_w;
        $src_img_h = $size_h;

        $degrees = $data['rotate'];

        // Rotate the source image
        if (is_numeric($degrees) && $degrees != 0) {
            // PHP's degrees is opposite to CSS's degrees
            $new_img = imagerotate($src_img, -$degrees, imagecolorallocatealpha($src_img, 0, 0, 0, 127));

            imagedestroy($src_img);
            $src_img = $new_img;

            $deg = abs($degrees) % 180;
            $arc = ($deg > 90 ? (180 - $deg) : $deg) * M_PI / 180;

            $src_img_w = $size_w * cos($arc) + $size_h * sin($arc);
            $src_img_h = $size_w * sin($arc) + $size_h * cos($arc);

            // Fix rotated image miss 1px issue when degrees < 0
            $src_img_w -= 1;
            $src_img_h -= 1;
        }

        $tmp_img_w = $data['width'];
        $tmp_img_h = $data['height'];

//        $dst_img_w = 220;
//        $dst_img_h = 220;

        $dst_img_w = $tmp_img_w;
        $dst_img_h = $tmp_img_h;

        $src_x = $data['x'];
        $src_y = $data['y'];

        if ($src_x <= -$tmp_img_w || $src_x > $src_img_w) {
            $src_x = $src_w = $dst_x = $dst_w = 0;
        } else if ($src_x <= 0) {
            $dst_x = -$src_x;
            $src_x = 0;
            $src_w = $dst_w = min($src_img_w, $tmp_img_w + $src_x);
        } else if ($src_x <= $src_img_w) {
            $dst_x = 0;
            $src_w = $dst_w = min($tmp_img_w, $src_img_w - $src_x);
        }

        if ($src_w <= 0 || $src_y <= -$tmp_img_h || $src_y > $src_img_h) {
            $src_y = $src_h = $dst_y = $dst_h = 0;
        } else if ($src_y <= 0) {
            $dst_y = -$src_y;
            $src_y = 0;
            $src_h = $dst_h = min($src_img_h, $tmp_img_h + $src_y);
        } else if ($src_y <= $src_img_h) {
            $dst_y = 0;
            $src_h = $dst_h = min($tmp_img_h, $src_img_h - $src_y);
        }

        // Scale to destination position and size
        $ratio = $tmp_img_w / $dst_img_w;
        $dst_x /= $ratio;
        $dst_y /= $ratio;
        $dst_w /= $ratio;
        $dst_h /= $ratio;

        $dst_img = imagecreatetruecolor($dst_img_w, $dst_img_h);

        // Add transparent background to destination image
        imagefill($dst_img, 0, 0, imagecolorallocatealpha($dst_img, 0, 0, 0, 127));
        imagesavealpha($dst_img, true);

        $result = imagecopyresampled($dst_img, $src_img, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);

        if ($result) {
            if (!imagepng($dst_img, $dst)) {
                $error = "Failed to save the cropped image file";
            }
        } else {
            $error = "Failed to crop the image file";
        }

        imagedestroy($src_img);
        imagedestroy($dst_img);
    } else {
        $error = "Failed to read the image file.";
    }

    return $error;
}

if (array_key_exists("data", $_POST)) {
    $data = $_POST['data'];
    crop("/path/to/image.jpg", "/path/to/image-cropped.jpg", $data);
}

Hopefully this might save some php developers some time. Cheers.

tomasr1981 commented 4 years ago

Hi lingtalfi, unfortunately is not top left pixel size (border is bordered with black background). Do you know where is problem with your script?

JS cropped: cropperjs-result

Server-side cropped: image-cropped

lingtalfi commented 4 years ago

Hi, I don't know what might be wrong with this script, but anyway I've switched to using js blob directly (and just use php's file_put_contents method on the server side).

In my case it was much simpler: because now the file can be modified in any ways the js gui can provide (for instance cropping, or image filtering), and it will always be pixel perfect.

Hope this helps.

tomasr1981 commented 4 years ago

I am sending values from cropper.getData() and that values I used within GD functions imagerotate and imagecrop. I try to do pixel perfect solution, but rotation destroy accuracy. Do you have some working example of your solution with js blob?

lingtalfi commented 4 years ago

Well, server side I just use a basic file_put_contents, you get the data from the $_FILES super array (but of course you should add security checkings...).

On the client side (js), it's based on the XMLHttpRequest object. There is a good resource here: https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications You might be interested in the section " Handling the upload process for a file ", and the whole page in general if you're not yet familiar with this js api.

I don't know if it will help you, but here is a snippet I use:


    /**
     * Note: at the time of writing (2020-04-06), the growing fetch api doesn't have a support
     * for upload progress monitoring yet (at least not that I know of), so this method uses
     * the older XMLHttpRequest api.
     */
    uploadFileProgress: async function (url, data, onProgress, decorator) {

        return new Promise((resolve, reject) => {

            let formdata;

            if (data instanceof FormData) {
                formdata = data;
            } else {
                formdata = new FormData();
                for (let i in data) {
                    formdata.append(i, data[i]);
                }
            }

            var ajax = new XMLHttpRequest();
            // ajax.overrideMimeType("application/json");

            if (this.isFunction(decorator)) {
                decorator(ajax);
            }

            ajax.upload.addEventListener("progress", function (e) {
                var percent = Math.round((e.loaded / e.total) * 100, 2);
                onProgress(e, percent, e.loaded, e.total);
            }, false);

            ajax.addEventListener("load", function (e) {
                e.stopPropagation();
                e.preventDefault();
            }, false);
            ajax.addEventListener("error", function (e) {
                e.stopPropagation();
                e.preventDefault();
                if ("onError" in options) {
                    reject(e);
                }
            }, false);
            ajax.addEventListener("abort", function (e) {
                e.stopPropagation();
                e.preventDefault();
                if ("onAbort" in options) {
                    reject(e);
                }
            }, false);

            ajax.open("POST", url);
            ajax.onreadystatechange = function () {
                if (ajax.readyState === 4) {
                    resolve(ajax);
                }
            };
            ajax.send(formdata);

        });
    },

As mentioned in the comments, I was looking for an upload with progress bar, if you don't need the progress bar, you can just use fetch which is more straightforward:

This page should be helpful: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

An example from this page is this:

const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');

formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);

fetch('https://example.com/profile/avatar', {
  method: 'PUT',
  body: formData
})
.then(response => response.json())
.then(result => {
  console.log('Success:', result);
})
.catch(error => {
  console.error('Error:', error);
});

So basically, you want to get your hand on the blob one way or the other. Note that a File object is a Blob in javascript (File extends Blob), so get the file/blob, do all your manipulations on it (jquery cropper, or other manipulations), then send the blob to your server and save it directly (that's what I do, no need for calculus server side).

Good luck.

fengyuanchen commented 2 years ago

Here is an early PHP example for reference.