168

I have found a few different posts and even questions on stackoverflow answering this question. I am basically implementing this same thing as this post.

So here is my issue. When I upload the photo, I also need to submit the rest of the form. Here is my html:

<form id="uploadImageForm" enctype="multipart/form-data">
  <input name="imagefile[]" type="file" id="takePictureField" accept="image/*" onchange="uploadPhotos(\'#{imageUploadUrl}\')" />
  <input id="name" value="#{name}" />
  ... a few more inputs ... 
</form>

Previously, I did not need to resize the image, so my javascript looked like this:

window.uploadPhotos = function(url){
    var data = new FormData($("form[id*='uploadImageForm']")[0]);

    $.ajax({
        url: url,
        data: data,
        cache: false,
        contentType: false,
        processData: false,
        type: 'POST',
        success: function(data){
            ... handle error...
            }
        }
    });
};

This all worked great... now that I need to resize the images... how can I replace the image in the form so that the resized one is posted and not the uploaded image?

window.uploadPhotos = function(url){

    var resizedImage;

    // Read in file
    var file = event.target.files[0];

    // Ensure it's an image
    if(file.type.match(/image.*/)) {
        console.log('An image has been loaded');

        // Load the image
        var reader = new FileReader();
        reader.onload = function (readerEvent) {
            var image = new Image();
            image.onload = function (imageEvent) {

                // Resize the image
                var canvas = document.createElement('canvas'),
                    max_size = 1200,
                    width = image.width,
                    height = image.height;
                if (width > height) {
                    if (width > max_size) {
                        height *= max_size / width;
                        width = max_size;
                    }
                } else {
                    if (height > max_size) {
                        width *= max_size / height;
                        height = max_size;
                    }
                }
                canvas.width = width;
                canvas.height = height;
                canvas.getContext('2d').drawImage(image, 0, 0, width, height);
                resizedImage = canvas.toDataURL('image/jpeg');
            }
            image.src = readerEvent.target.result;
        }
        reader.readAsDataURL(file);
    }


   // TODO: Need some logic here to switch out which photo is being posted...

    var data = new FormData($("form[id*='uploadImageForm']")[0]);

    $.ajax({
        url: url,
        data: data,
        cache: false,
        contentType: false,
        processData: false,
        type: 'POST',
        success: function(data){
            ... handle error...
            }
        }
    });
};

I've thought about moving the file input out of the form and having a hidden input in the form that I set the value of to the value of the resized image... But I'm wondering if I can just replace the image that is already in the form.

Community
  • 1
  • 1
ferics2
  • 5,241
  • 7
  • 30
  • 46
  • Are you working with any server side language or only html5 and javascript? – luke2012 May 30 '14 at 01:12
  • 1
    @luke2012 java server side – ferics2 May 30 '14 at 01:20
  • Maybe crop the image on the client side using something like jCrop then send the coordinates to the server side and crop it. i.e `BufferedImage dest = src.getSubimage(rect.x, rect.y, rect.width, rect.height);` – luke2012 May 30 '14 at 01:25
  • 9
    @luke2012 the point is to reduce the image size BEFORE sending it to the server. – ferics2 May 30 '14 at 02:29
  • Take a look at the js source of http://pandamatak.com/people/anand/test/crop/ seems to be similar.. – luke2012 May 30 '14 at 02:50
  • can anyone tell me, is` canvas` required to resize the image?, cant we resize the `image`, just by selecting the `image` with `document.querySelector("image")` and get the `width` and the `height` , and `resize` it. – Rohan Devaki Nov 14 '20 at 06:35
  • More often we use a form with a submit button. So it maybe were a more generic solution to do it this way: 1-st step: resize the image and persist it 2-nd step: use the resized file on submit insted to use the original uploaded file – Hermann Schwarz Feb 13 '22 at 12:38

6 Answers6

228

Here is what I ended up doing and it worked great.

First I moved the file input outside of the form so that it is not submitted:

<input name="imagefile[]" type="file" id="takePictureField" accept="image/*" onchange="uploadPhotos(\'#{imageUploadUrl}\')" />
<form id="uploadImageForm" enctype="multipart/form-data">
    <input id="name" value="#{name}" />
    ... a few more inputs ... 
</form>

Then I changed the uploadPhotos function to handle only the resizing:

window.uploadPhotos = function(url){
    // Read in file
    var file = event.target.files[0];

    // Ensure it's an image
    if(file.type.match(/image.*/)) {
        console.log('An image has been loaded');

        // Load the image
        var reader = new FileReader();
        reader.onload = function (readerEvent) {
            var image = new Image();
            image.onload = function (imageEvent) {

                // Resize the image
                var canvas = document.createElement('canvas'),
                    max_size = 544,// TODO : pull max size from a site config
                    width = image.width,
                    height = image.height;
                if (width > height) {
                    if (width > max_size) {
                        height *= max_size / width;
                        width = max_size;
                    }
                } else {
                    if (height > max_size) {
                        width *= max_size / height;
                        height = max_size;
                    }
                }
                canvas.width = width;
                canvas.height = height;
                canvas.getContext('2d').drawImage(image, 0, 0, width, height);
                var dataUrl = canvas.toDataURL('image/jpeg');
                var resizedImage = dataURLToBlob(dataUrl);
                $.event.trigger({
                    type: "imageResized",
                    blob: resizedImage,
                    url: dataUrl
                });
            }
            image.src = readerEvent.target.result;
        }
        reader.readAsDataURL(file);
    }
};

As you can see I'm using canvas.toDataURL('image/jpeg'); to change the resized image into a dataUrl adn then I call the function dataURLToBlob(dataUrl); to turn the dataUrl into a blob that I can then append to the form. When the blob is created, I trigger a custom event. Here is the function to create the blob:

/* Utility function to convert a canvas to a BLOB */
var dataURLToBlob = function(dataURL) {
    var BASE64_MARKER = ';base64,';
    if (dataURL.indexOf(BASE64_MARKER) == -1) {
        var parts = dataURL.split(',');
        var contentType = parts[0].split(':')[1];
        var raw = parts[1];

        return new Blob([raw], {type: contentType});
    }

    var parts = dataURL.split(BASE64_MARKER);
    var contentType = parts[0].split(':')[1];
    var raw = window.atob(parts[1]);
    var rawLength = raw.length;

    var uInt8Array = new Uint8Array(rawLength);

    for (var i = 0; i < rawLength; ++i) {
        uInt8Array[i] = raw.charCodeAt(i);
    }

    return new Blob([uInt8Array], {type: contentType});
}
/* End Utility function to convert a canvas to a BLOB      */

Finally, here is my event handler that takes the blob from the custom event, appends the form and then submits it.

/* Handle image resized events */
$(document).on("imageResized", function (event) {
    var data = new FormData($("form[id*='uploadImageForm']")[0]);
    if (event.blob && event.url) {
        data.append('image_data', event.blob);

        $.ajax({
            url: event.url,
            data: data,
            cache: false,
            contentType: false,
            processData: false,
            type: 'POST',
            success: function(data){
               //handle errors...
            }
        });
    }
});
MikeT
  • 2,530
  • 25
  • 36
ferics2
  • 5,241
  • 7
  • 30
  • 46
  • 1
    Wonderful code, after some tweaking and fixing some errors (I don't remember exactly what and which) I got it to work. By the way, I think when you wrote `width *= max_size / width;` you actually meant `width *= max_size / height;`. – user1111929 May 07 '15 at 22:16
  • 3
    Does this code work on mobile devices? Under iOS and Android? – planewalker Sep 14 '15 at 21:59
  • 4
    @planewalker I actually wrote the code specifically for mobile devices. To cut down on data usage. – ferics2 Sep 15 '15 at 16:42
  • @ferics2 so the FormData and Blob 'classes' are available in Mobile Safari? I'm asking because of the table here: https://developer.mozilla.org/en-US/docs/Web/API/FormData There we have only question marks for some of the browsers – planewalker Sep 16 '15 at 17:02
  • 2
    @planewalker, I was testing on iOS when I wrote this. Hope it works for you. – ferics2 Sep 18 '15 at 00:45
  • Worked great for me. Had to change a few things to work with Angular instead of plain jQuery but not much. Thanks! – RebelFist Oct 05 '15 at 22:23
  • 4
    Well done and thanks for sharing! (The minor error others have mentioned but not identified is in the image resizing event trigger. "url : url" should be "url: dataUrl") – Seoras Oct 21 '15 at 00:59
  • 1
    Everytime I run this the image does not decrease in size once it reaches the server? I even set `max_size = 0.5` and the files still remain full size once it has reached the server? – Rossco Jul 14 '16 at 19:15
  • Saving over 1Mb images in server via FileReader wasn't working on mobile devices, only on desktop... thanks to this code and with some modifications, it works now. Thanks ferics2. – JohnA10 Jul 26 '16 at 18:05
  • Great script, is there a away to send new resized image to server php file using either javascript or HTML – INOH Sep 20 '16 at 05:31
  • 2
    You can reduce the size further if you set the jpg quality: `var dataUrl = canvas.toDataURL('image/jpeg',.6);` the default quality seems to be .92 in most browsers. – pathfinder Sep 21 '16 at 04:06
  • @Seoras I think you are mistaken; It should be `url: url`—the url that the data is uploaded to. It's a parameter of `window.uploadPhotos`. I've edited it to be `upload_url` for clarity. – Shammel Lee Sep 22 '16 at 18:39
  • 1
    Iam confused on the event.url where do i iput my path to my php script – INOH Sep 24 '16 at 13:26
  • 1
    This is a really beautiful little piece of code. Works great. Thank you so much. Saves a lot of bandwidth and thus frustration! – cjastram Jan 08 '18 at 20:05
  • 2
    For those who might be working with firebase what you upload to cloud storage is the `resizedImage`. Keep the rest of the code but you can delete the `$.event.trigger ` part. Other than that this code works flawlessly! – Mikebarson Jan 22 '18 at 07:17
  • Is the code above supposed to create a window/preview panel that allows you to resize? because I just copied the code above to test it for my own website and when I select image, nothing happens, it only shows the selected image name in the form but no preview panel created –  Mar 25 '18 at 23:28
  • 1
    @Foobarer no, it does not. It just resizes it based on the values set in the code. – ferics2 Mar 29 '18 at 04:15
  • I think this method would strip EXIF data like ICC profiles, geolocation and rotation information - this would be especially problematic on iOS. Anyone know an easy way to transfer the EXIF data from the original image, to the upload image? – Kevin Newman Jul 26 '18 at 00:56
  • For certain large JPEG files attaching the dataURI to the image results in an error. Is there a way to avoid that or just understand where it went wrong? – Jai Sandhu Aug 31 '18 at 19:23
  • Would you ever consider making this an `npm` package? – Luke Pighetti Nov 03 '19 at 20:29
  • what language is this , like `$(document).on`, i have never seen this in `javascript`, can anonye answer this with the `javascript`, here is the question link. https://stackoverflow.com/q/64831445/12793212 – Rohan Devaki Nov 14 '20 at 06:36
  • 1
    I believe you can replace the `dataURLToBlob` function with `canvas.toBlob`. – Oliver Joseph Ash Nov 16 '20 at 15:01
  • Thank you for this code snippet. This does not seem to work when uploading multiple images. For example, if I select two images from my desktop to upload. How can I modify it to handle that case? – oky_sabeni Jan 09 '21 at 13:41
88

if any interested I've made a typescript version:

interface IResizeImageOptions {
  maxSize: number;
  file: File;
}
const resizeImage = (settings: IResizeImageOptions) => {
  const file = settings.file;
  const maxSize = settings.maxSize;
  const reader = new FileReader();
  const image = new Image();
  const canvas = document.createElement('canvas');
  const dataURItoBlob = (dataURI: string) => {
    const bytes = dataURI.split(',')[0].indexOf('base64') >= 0 ?
        atob(dataURI.split(',')[1]) :
        unescape(dataURI.split(',')[1]);
    const mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
    const max = bytes.length;
    const ia = new Uint8Array(max);
    for (var i = 0; i < max; i++) ia[i] = bytes.charCodeAt(i);
    return new Blob([ia], {type:mime});
  };
  const resize = () => {
    let width = image.width;
    let height = image.height;

    if (width > height) {
        if (width > maxSize) {
            height *= maxSize / width;
            width = maxSize;
        }
    } else {
        if (height > maxSize) {
            width *= maxSize / height;
            height = maxSize;
        }
    }

    canvas.width = width;
    canvas.height = height;
    canvas.getContext('2d').drawImage(image, 0, 0, width, height);
    let dataUrl = canvas.toDataURL('image/jpeg');
    return dataURItoBlob(dataUrl);
  };

  return new Promise((ok, no) => {
      if (!file.type.match(/image.*/)) {
        no(new Error("Not an image"));
        return;
      }

      reader.onload = (readerEvent: any) => {
        image.onload = () => ok(resize());
        image.src = readerEvent.target.result;
      };
      reader.readAsDataURL(file);
  })    
};

and here's the javascript result:

var resizeImage = function (settings) {
    var file = settings.file;
    var maxSize = settings.maxSize;
    var reader = new FileReader();
    var image = new Image();
    var canvas = document.createElement('canvas');
    var dataURItoBlob = function (dataURI) {
        var bytes = dataURI.split(',')[0].indexOf('base64') >= 0 ?
            atob(dataURI.split(',')[1]) :
            unescape(dataURI.split(',')[1]);
        var mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
        var max = bytes.length;
        var ia = new Uint8Array(max);
        for (var i = 0; i < max; i++)
            ia[i] = bytes.charCodeAt(i);
        return new Blob([ia], { type: mime });
    };
    var resize = function () {
        var width = image.width;
        var height = image.height;
        if (width > height) {
            if (width > maxSize) {
                height *= maxSize / width;
                width = maxSize;
            }
        } else {
            if (height > maxSize) {
                width *= maxSize / height;
                height = maxSize;
            }
        }
        canvas.width = width;
        canvas.height = height;
        canvas.getContext('2d').drawImage(image, 0, 0, width, height);
        var dataUrl = canvas.toDataURL('image/jpeg');
        return dataURItoBlob(dataUrl);
    };
    return new Promise(function (ok, no) {
        if (!file.type.match(/image.*/)) {
            no(new Error("Not an image"));
            return;
        }
        reader.onload = function (readerEvent) {
            image.onload = function () { return ok(resize()); };
            image.src = readerEvent.target.result;
        };
        reader.readAsDataURL(file);
    });
};

usage is like:

resizeImage({
    file: $image.files[0],
    maxSize: 500
}).then(function (resizedImage) {
    console.log("upload resized image")
}).catch(function (err) {
    console.error(err);
});

or (async/await):

const config = {
    file: $image.files[0],
    maxSize: 500
};
const resizedImage = await resizeImage(config)
console.log("upload resized image")
Santiago Hernández
  • 5,438
  • 2
  • 26
  • 34
10

If some of you, like me, encounter orientation problems I have combined the solutions here with a exif orientation fix

https://gist.github.com/SagiMedina/f00a57de4e211456225d3114fd10b0d0

Sagi Medina
  • 915
  • 1
  • 9
  • 11
7

In 2022 we have some new APIs available to us. This is the solution I came up with. We don't have to mess with FileReader API or image onload callback.

The following code accepts a file object or a Blob object and outputs a blob of a cropped, centered, resized image, and also converts it to webp.


export default async (file, size) => {
  size ??= 256

  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')

  canvas.width = size
  canvas.height = size

  const bitmap = await createImageBitmap(file)
  const { width, height } = bitmap

  const ratio = Math.max(size / width, size / height)

  const x = (size - (width * ratio)) / 2
  const y = (size - (height * ratio)) / 2

  ctx.drawImage(bitmap, 0, 0, width, height, x, y, width * ratio, height * ratio)

  return new Promise(resolve => {
    canvas.toBlob(blob => {
      resolve(blob)
    }, 'image/webp', 1)
  })
}
whaley
  • 598
  • 4
  • 12
3

I have made my own version without actually using the file reader. Instead I use createObjectUrl which is supported by majority of modern browsers.

/**
 * Function scaling an image from a file input to specified dimensions
 * If the specified dimensions are not proportional to image dimensions the output image will be cropped at center
 *
 * @param file {File} Input file of a form
 * @param dimensions {{width: number, height: number}} Dimenstions of the output image
 * @returns {Promise<Blob | null>} Promise resolving to a scale image or a null if provided an invalid file type
 */
export async function scaleImageBeforeUpload(file: File, dimensions: {width: number, height: number}): Promise<Blob | null> {
    // ensure the file is an image
    if (!file.type.match(/image.*/)) return null;

    const image = new Image();
    image.src = URL.createObjectURL(file);

    await new Promise<Event>((res) => image.onload = res);
    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d", {alpha: true});

    canvas.width = dimensions.width;
    canvas.height = dimensions.height;

    if (image.height <= image.width) {
        const scaleProportions = canvas.height / image.height;
        const scaledWidth = scaleProportions * image.width;
        context.drawImage(image, (canvas.width - scaledWidth)/2, 0, scaledWidth, canvas.height);
    }
    else {
        const scaleProportions = canvas.width / image.width;
        const scaledHeight = scaleProportions * image.height;
        context.drawImage(image, 0, (canvas.height - scaledHeight)/2, canvas.width, scaledHeight);
    }

    return new Promise((res) => canvas.toBlob(res));
}
Krzysztof Krzeszewski
  • 5,912
  • 2
  • 17
  • 30
  • 1
    Excellent solution, just note that your resizing logic results in a cropping of the image. I would use the same logic as in @ferics2 answer. – i.brod Jul 01 '22 at 16:26
  • 1
    His logic defines a max size for the dimensions, while mine specifies exactly the output, his approach may be reasonable, but i still think mine has a reason. – Krzysztof Krzeszewski Jul 02 '22 at 10:30
0

worlds most inefficient way to resize an image to fit within a maximum file size

    function processImage(base64) {
        return new Promise(function(resolve, reject) {
            var img = new Image();
            img.onload = function() {
                var canvas = document.createElement('canvas');
                var ctx = canvas.getContext('2d');
                var width = img.width;
                var height = img.height;
                var resizedBase64 = null;
                while (resizedBase64 == null) {
                    console.log("width: " + width + " height: " + height);
                    canvas.width = width;
                    canvas.height = height;
                    ctx.drawImage(img, 0, 0, width, height);
                    if (canvas.toDataURL('image/png').length > maxFileSize) {
                        width = width * 0.9;
                        height = height * 0.9;
                    } else {
                        resizedBase64 = canvas.toDataURL('image/png')
                    }
                }
                console.log("width: " + width + " height: " + height);
                resolve(resizedBase64);
            };
            img.src = base64;
        });
    }
kaioker2
  • 297
  • 3
  • 11