48

I have a form which allows the user to upload a picture. After the user has submitted the form, I'd like to generate on the front-end a thumbnail for each picture and then store it on server.

For security reason it's not possible to alter the value of a file input field, so how could I send to server some thumbnails images generated on the front-end in js?

Is it possible on front-end to generate a thumbnail from the image set in the input file field before form submit? And then submitting both at same time?

Alessandro Incarnati
  • 7,018
  • 3
  • 38
  • 54
  • You could upload the picture to a temporary folder after the form is submitted. Then append an `` element with a `src` set to the path of the temporarily stored picture. If the user likes it, they can confirm and you can upload it to the real storage area, using the new path – Ian May 11 '13 at 19:22
  • I don't need to get it confirmed, I simply need to upload one image and its thumbnail to server. Is it possible on front-end to generate a thumbnail from the image set in the input file field before form submit? And then submitting both at same time? – Alessandro Incarnati May 11 '13 at 19:28
  • There's an example [here](http://stackoverflow.com/a/7557690/161632) of using the canvas tag to display a thumbnail. Otherwise, you'd need some server-side processing to generate a thumbnail version of the uploaded image. – Netricity May 11 '13 at 19:44

6 Answers6

64

I found This simpler yet powerful tutorial. It simply creates an img element and, using the fileReader object, assigns its source attribute as the value of the form input

function previewFile() {
  var preview = document.querySelector('img');
  var file    = document.querySelector('input[type=file]').files[0];
  var reader  = new FileReader();

  reader.onloadend = function () {
    preview.src = reader.result;
  }

  if (file) {
    reader.readAsDataURL(file);
  } else {
    preview.src = "";
  }
}
<input type="file" onchange="previewFile()"><br>
<img src="" height="200" alt="Image preview...">
Cedric Ipkiss
  • 5,662
  • 2
  • 43
  • 72
26

After a better search online I found the answer to my question.

It is possible to combine canvas together with the File API.

Try to upload any picture in the demo below and see that a new generated thumbnail will appear on the right side of the form.

DEMO: http://jsfiddle.net/a_incarnati/fua75hpv/

function handleImage(e){
    var reader = new FileReader();
    reader.onload = function(event){
        var img = new Image();
        img.onload = function(){
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img,0,0);
        }
        img.src = event.target.result;
    }
    reader.readAsDataURL(e.target.files[0]);     
}

A good answer has been given by DerekR to this question:

How to upload image into HTML5 canvas

Community
  • 1
  • 1
Alessandro Incarnati
  • 7,018
  • 3
  • 38
  • 54
  • 4
    Your example does not use the [File System API](http://www.w3.org/TR/file-system-api/). It uses the [File API](http://www.w3.org/TR/FileAPI/). – Ray Nicholus May 14 '13 at 17:34
  • 1
    And multiply with a number less than 1 to the img.width and img.height parameters to scale down when assigning to canvas.width and canvas.height. Eg. canvas.width=img.width*0.3. – treeFan Dec 30 '16 at 10:05
  • And then something like ctx.drawImage(img,0,0,canvas.width,canvas.height). – treeFan Dec 30 '16 at 10:12
17

Building on top of what Allesandro wrote to something more pragmatic.

The function takes a file from the File API and tries to fit it in the boundBox while preserving the aspect ratio. Nothing is drawn, but instead you get back a Promise that spits the dataUrl generated.

// Creates a thumbnail fitted insize the boundBox (w x h)
generateThumbnail(file, boundBox) {
  if (!boundBox || boundBox.length != 2){
    throw "You need to give the boundBox"
  }
  const canvas = document.createElement("canvas")
  const ctx = canvas.getContext('2d')
  if (!ctx) {
    throw new Error('Context not available')
  }

  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onerror = reject
    img.onload = function() {
      const scaleRatio = Math.min(...boundBox) / Math.max(img.width, img.height)
      const w = img.width * scaleRatio
      const h = img.height * scaleRatio
      canvas.width = w
      canvas.height = h
      ctx.drawImage(img, 0, 0, w, h)
      return resolve(canvas.toDataURL(file.type))
    }
    img.src = window.URL.createObjectURL(file)
  })
}

It can be used like below

generateThumbnail(file, [300, 300]).then(function(dataUrl){
    console.log(dataUrl)
})
Janne Annala
  • 25,928
  • 8
  • 31
  • 41
Pithikos
  • 18,827
  • 15
  • 113
  • 136
  • Can you explain why you are using the ratio of `Math.min(...boundBox)` and `Math.max(...realImageDimensions)` ? That is only going to work for exactly square bounding boxes. Did you test with e.g. `[300, 100]` ? – Phil Jun 28 '21 at 15:55
  • @Phil essentially it takes the biggest edge from the actual image and works out the scale ratio required to extend it to the smallest edge of boundBox. This just works out so that the image will always fit inside the boundBox. Easier to check with pen and paper – Pithikos Jun 28 '21 at 20:28
  • For example, if the image = 600x200, Box = 300x100. ScaleRatio should be 0.5, not 0.166. You end up with a super tiny thumbnail of 100x17 – Phil Jun 28 '21 at 22:22
  • @Phil with a 0.166 ratio you get a 96x32 image (600*0.166 & 200*0.166). Remember the scale ratio is for downsizing. However you might right, the algorithm might be over-downsizing but it ensures that any edge is not above the minimum boundbox in this case (which can be what you want in many cases). – Pithikos Jun 29 '21 at 08:28
  • Yes I understand what a scale ratio is. No 1/6 of 600 = 100, not 96, which is too small. Why do you ask for a 2 argument bounding box, but only using Math.min(boundBox)? Cmon, the algorithm over downsizes which is clearly a bug and not what is suggested by the function name or arguments. Normally you would want a thumbnail to fit in the box you give. Downvoted! – Phil Jun 29 '21 at 22:41
  • 2
    @Phil you're always free to post a better answer if you have a better solution in mind or provide an edit – Pithikos Jun 30 '21 at 14:49
  • 1
    @Pithikos What about calculating both x- and y-ratio and keep the smallest one? ==> `var scaleRatio = Math.min( boundBox[0]/img.width , boundBox[1]/img.height , 1);` I also included a "1" in the min comparison, so the result will never be greater than 1 (no upscale, only downscale awaited). – fpierrat Jul 20 '21 at 09:34
  • 1
    I would also round down the calculated width and height (or I'm afraid it could in some cases exceed the boundbox by one pix): `let w = Math.floor(img.width*scaleRatio);` `let h = Math.floor(img.height*scaleRatio);` – fpierrat Jul 20 '21 at 09:38
  • Good answer thanks! It could be improved a bit though. The FileReader is completely unnecessary because you already have a File object. I would suggest to remove the reader and use `img.src = window.URL.createObjectURL(file)`. I would also add `img.onerror = reject` – Janne Annala Feb 23 '23 at 10:12
2

Thought it might be worth adding a more contemporary answer and citing MDN Web Docs.

You can add an event listener for "change" on the input element and then show a thumbnail of the selected image by accessing the file list through this.files (as shown in an MDN examples). Here is a recent implementation of mine. uploadWatermark is an <input type="file></input>

uploadWatermark.addEventListener('change', function(){
  const file = this.files[0];
  if (file.type.startsWith('image/')) {
    const img = document.createElement('img');
    const watermarkPreview = document.getElementById("uploaded-watermark");

    img.classList.add("prev-thumb");
    img.file = file;
    watermarkPreview.appendChild(img);

    const reader = new FileReader();
    reader.onload = (function(aImg) { return function(e) { aImg.src =   e.target.result; }})(img);
    reader.readAsDataURL(file);
  }
  
});
Smitty
  • 1,765
  • 15
  • 22
  • 1
    This probably becomes rather slow if you upload multiple large images. That's why thumbnails are useful. – Pithikos May 12 '20 at 09:20
1

TL;DR: See the JSFiddle

As I wanted to upload images via an API and show a preview of the image (two things that actually lended themselves well to each other), I came up with this:

(function(angular) {
    angular
        .module('app')
        .directive('inputFilePreview', [function() {

            var canvas, mapToModel, elementScope;

            /**
             * To be fired when the image has been loaded
             */
            var imageOnLoad = function(){
                canvas.width = this.width;
                canvas.height = this.height;
                canvas.getContext("2d").drawImage(this,0,0);
            };

            /**
             * To be fired when the FileReader has loaded
             * @param loadEvent {{}}
             */
            var readerOnLoad = function(loadEvent){
                var img = new Image();
                img.onload = imageOnLoad;
                img.src = loadEvent.target.result;
                if(mapToModel) {
                    setModelValue(elementScope, mapToModel, img.src);
                }
            };

            /**
             * This allows us to set the value of a model in the scope of the element (or global scope if the
             * model is an object)
             * @param scope {{}}
             * @param modelReference {string}
             * @param value {*}
             */
            var setModelValue = function(scope, modelReference, value) {
                // If the model reference refers to the propery of an object (eg. "object.property")
                if(~modelReference.indexOf('.')) {
                    var parts = modelReference.split('.', 2);
                    // Only set the value if that object already exists
                    if(scope.hasOwnProperty(parts[0])) {
                        scope[parts[0]][parts[1]] = value;
                        return;
                    }
                }
                scope[modelReference] = value;
            };

            /**
             * The logic for our directive
             * @param scope {{}}
             * @param element {{}}
             * @param attributes {{}}
             */
            var link = function(scope, element, attributes) {
                elementScope = scope;
                canvas = document.getElementById(attributes.inputFilePreview);
                if(attributes.hasOwnProperty('mapToModel')) {
                    mapToModel = attributes.mapToModel;
                }
                element.on('change', function(changeEvent) {
                    var reader = new FileReader();
                    reader.onload = readerOnLoad;
                    reader.readAsDataURL(changeEvent.target.files[0]);
                });
            };

            return {
                restrict: 'A',
                link: link
            };
        }]);
})(angular);

The two elements needed for the preview to work are:

<canvas id="image-preview"></canvas>
<input type="file" data-input-file-preview="image-preview" data-map-to-model="image.file" />

Snippet Follows:

(function (angular) {
    angular.module('app', [])
        .directive('inputFilePreview', [function () {

        var canvas, mapToModel, elementScope;

        /**
         * To be fired when the image has been loaded
         */
        var imageOnLoad = function () {
            canvas.width = this.width;
            canvas.height = this.height;
            canvas.getContext("2d").drawImage(this, 0, 0);
        };

        /**
         * To be fired when the FileReader has loaded
         * @param loadEvent {{}}
         */
        var readerOnLoad = function (loadEvent) {
            var img = new Image();
            img.onload = imageOnLoad;
            img.src = loadEvent.target.result;
            if (mapToModel) {
                setModelValue(elementScope, mapToModel, img.src);
            }
        };

        /**
         * This allows us to set the value of a model in the scope of the element (or global scope if the
         * model is an object)
         * @param scope {{}}
         * @param modelReference {string}
         * @param value {*}
         */
        var setModelValue = function (scope, modelReference, value) {
            // If the model reference refers to the propery of an object (eg. "object.property")
            if (~modelReference.indexOf('.')) {
                var parts = modelReference.split('.', 2);
                // Only set the value if that object already exists
                if (scope.hasOwnProperty(parts[0])) {
                    scope[parts[0]][parts[1]] = value;
                    return;
                }
            }
            scope[modelReference] = value;
        };

        /**
         * The logic for our directive
         * @param scope {{}}
         * @param element {{}}
         * @param attributes {{}}
         */
        var link = function (scope, element, attributes) {
            elementScope = scope;
            canvas = document.getElementById(attributes.inputFilePreview);
            if (attributes.hasOwnProperty('mapToModel')) {
                mapToModel = attributes.mapToModel;
            }
            element.on('change', function (changeEvent) {
                var reader = new FileReader();
                reader.onload = readerOnLoad;
                reader.readAsDataURL(changeEvent.target.files[0]);
            });
        };

        return {
            restrict: 'A',
            link: link
        };
    }])
        .controller('UploadImageController', [
        '$scope',

    function ($scope) {

        $scope.image = {
            title: 'Test title'
        };

        $scope.send = function (data) {
            $scope.sentData = JSON.stringify(data, null, 2);
            return false;
        };
    }]);
})(angular);
canvas {
    max-height: 300px;
    max-width: 300px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<form data-ng-app="app" data-ng-controller="UploadImageController">
    <input data-ng-model="image.title" />
    <br />
    <canvas id="image-preview"></canvas>
    <br />
    <input type="file" data-input-file-preview="image-preview" data-map-to-model="image.file" />
    <br />
    <input type="submit" data-ng-click="send(image)" />
    
    <pre>{{sentData}}</pre>
</form>
DanielM
  • 6,380
  • 2
  • 38
  • 57
0

You can achieve it with URL.createObjectUrl().

  const handleImage = (e) => {
    const file = e.target.files[0]
    const url = URL.createObjectURL(file)

    return url
  }

  const addBackground = (url) => {
    document.querySelector('.wrapper').style.background = `url("${url}")`;
  }

<div className='wrapper'></div>

Caution

URL.createObjectUrl() can cause memory leak, so you have to release it by URL.revokeObjectURL() after using url.

ho-s
  • 89
  • 1
  • 10