82

I need a way to resize pictures in JavaScript without using an HTML element.

My mobile HTML app capture photos and then converts them into base64 strings. Finally I want to resize them before they are sent to the API.

I'm looking for a different and more suitable way to resize than using a canvas element, is there a way?

0xRLA
  • 3,279
  • 4
  • 30
  • 42
  • Canvas is not suitable for manipulating images? –  Jan 06 '14 at 21:45
  • You can create an off-screen canvas without inserting it into DOM. I can make an example if this is interesting. It is at least much faster than encoding using js alone as the canvas can do this in native compiled code. –  Jan 07 '14 at 07:07
  • Sounds great Ken, I would be very interested in that! :) – 0xRLA Jan 07 '14 at 07:12
  • 2
    I'd be interested in this as well because here's the deal with Canvas and mobile ... Memory. You can't load in larger images and "larger" these days is actually the size of what the mobile device's camera takes since everyone likes cramming 50 million megapixels into a phone these days :) – Tom Jan 22 '14 at 23:22
  • Tom. I never found a good solution for this. I came around this problem by simply setting the picture size and quality in the phonegap camera function. Look in phonegap camera docs. It's also possible when importing pictures trough the gallery. – 0xRLA Jan 23 '14 at 07:32
  • read my post guys)) @Tom – Alex Nikulin Jun 06 '17 at 06:38
  • Also, you can send resized image like a file, not a base64 string? Are you interested? – Alex Nikulin Jun 06 '17 at 06:47

8 Answers8

92

A way to avoid the main HTML to be affected is to create an off-screen canvas that is kept out of the DOM-tree.

This will provide a bitmap buffer and native compiled code to encode the image data. It is straight forward to do:

function imageToDataUri(img, width, height) {

    // create an off-screen canvas
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d');

    // set its dimension to target size
    canvas.width = width;
    canvas.height = height;

    // draw source image into the off-screen canvas:
    ctx.drawImage(img, 0, 0, width, height);

    // encode image to data-uri with base64 version of compressed image
    return canvas.toDataURL();
}

If you want to produce a different format than PNG (default) just specify the type like this:

return canvas.toDataURL('image/jpeg', quality);  // quality = [0.0, 1.0]

Worth to note that CORS restrictions applies to toDataURL().

If your app is giving only base64 encoded images (I assume they are data-uri's with base64 data?) then you need to "load" the image first:

var img = new Image;

img.onload = resizeImage;
img.src = originalDataUriHere;

function resizeImage() {
    var newDataUri = imageToDataUri(this, targetWidth, targetHeight);
    // continue from here...
}

If the source is pure base-64 string simply add a header to it to make it a data-uri:

function base64ToDataUri(base64) {
    return 'data:image/png;base64,' + base64;
}

Just replace the image/png part with the type the base64 string represents (ie. make it an optional argument).

31

Ken's answer is the right answer, but his code doesn't work. I made some adjustments on it and it now works perfectly. To resize a Data URI :

// Takes a data URI and returns the Data URI corresponding to the resized image at the wanted size.
function resizedataURL(datas, wantedWidth, wantedHeight)
    {
        // We create an image to receive the Data URI
        var img = document.createElement('img');

        // When the event "onload" is triggered we can resize the image.
        img.onload = function()
            {        
                // We create a canvas and get its context.
                var canvas = document.createElement('canvas');
                var ctx = canvas.getContext('2d');

                // We set the dimensions at the wanted size.
                canvas.width = wantedWidth;
                canvas.height = wantedHeight;

                // We resize the image with the canvas method drawImage();
                ctx.drawImage(this, 0, 0, wantedWidth, wantedHeight);

                var dataURI = canvas.toDataURL();

                /////////////////////////////////////////
                // Use and treat your Data URI here !! //
                /////////////////////////////////////////
            };

        // We put the Data URI in the image's src attribute
        img.src = datas;
    }
// Use it like that : resizedataURL('yourDataURIHere', 50, 50);
Pierrick Martellière
  • 1,554
  • 3
  • 21
  • 42
26

Pierrick Martellière is far the best answer, I just wanted to point that you should implement that with a async function. Once that, you would be able to do something like:

var newDataUri = await resizedataURL(datas,600,600);

This will wait for the result of the function before going to the next step. It's a cleaner way to write code. Here is the function from Pierrick with the little edit:

// Takes a data URI and returns the Data URI corresponding to the resized image at the wanted size.
function resizedataURL(datas, wantedWidth, wantedHeight){
    return new Promise(async function(resolve,reject){

        // We create an image to receive the Data URI
        var img = document.createElement('img');

        // When the event "onload" is triggered we can resize the image.
        img.onload = function()
        {        
            // We create a canvas and get its context.
            var canvas = document.createElement('canvas');
            var ctx = canvas.getContext('2d');

            // We set the dimensions at the wanted size.
            canvas.width = wantedWidth;
            canvas.height = wantedHeight;

            // We resize the image with the canvas method drawImage();
            ctx.drawImage(this, 0, 0, wantedWidth, wantedHeight);

            var dataURI = canvas.toDataURL();

            // This is the return of the Promise
            resolve(dataURI);
        };

        // We put the Data URI in the image's src attribute
        img.src = datas;

    })
}// Use it like : var newDataURI = await resizedataURL('yourDataURIHere', 50, 50);

For more details you can check MDN Docs : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Promise

  • 8
    The question specifically says "without canvas". This is not an answer. Downvoted. – Adam Barnes May 08 '19 at 17:00
  • 4
    @AdamBarnes I hope you downvoted all the others answers then ;) and if you read the first 2 or 3 comments of the question you can actually read that the goal is to not insert any element on the DOM and do it behind (backend). Cheers Fella ! –  May 10 '19 at 07:57
  • This solution doesn't consider images uploaded with mobile camera and might have problems with orientation, e.g. images might be rotated. – pavloko Apr 10 '20 at 20:20
  • 1
    @pavloko I wrote an image upload util that did parse the orientation tag from the image, rotate it, and set a new orientation tag. Then Google Chrome updated to start respecting the orientation tag. It may not be worth the effort, since that was one of the last popular browsers to add support for orientation. We ended up having to strip it out, because it wasn't worth fighting what the browser would do. I think the only time orientation is that critical is when it will be manipulated in the backend by a lib that does not respect or support orientation. – Chris Baker Oct 25 '20 at 02:21
  • @AdamBarnes A good answer to a technical problem does not always try to incorporate the asker's pre-conceived notions. The OP said they didn't want a canvas element, but didn't say why. One MAY assume they are operating under the incorrect notion that you must add the canvas element to DOM, which isn't the case. Bad downvote, this is a decent solution, even if it doesn't strictly cover the OP's unspecified restriction. Not a fan of the hard-coded aspect. It might not cover OP's case, but it's still a good solution for those that COULD use the canvas element and thought they had to avoid it. – Chris Baker Oct 25 '20 at 02:25
  • I can recommend to also implement `img.onerror = reject;` to fulfil the promise in all cases. – Thomas Kekeisen Jun 02 '22 at 16:11
11

Yes, you can. These solutions good for resizing not just converting image to base64.

  1. You can convert js file to image bitmap by jpg-js.And you can resize only by this lib, but in a case of resizing from very large image to very small, quality will be very bad.Best way for high-res images is to convert file to bitmap by jpg-js and then resize this bitmap by Pica lib.
  2. You can get image data from a file by jpg-js (or draw an image on canvas)and then resize canvasImageData by resizing lib pica. (good for High-resolution images, without canvas size restriction)
  3. You can use offscreen canvas, without attaching the canvas to a body, and resize an image. This solution will be faster but will be the worse solution for high-resolution images, for example 6000x6000 pixels. In that case, result canvas can be with bad quality or just empty, or browser can fall with memory limit exception. (good for normal and small images)

Jpg-js and Pica will not use dom elements at all. These libs are working only with image data, without dom elements (canvas and image).

About the canvas, size restriction see this post

Alex Nikulin
  • 8,194
  • 4
  • 35
  • 37
  • Thank you for actually answering the question. I'm not sure why people vote-up answers that rely on canvas, despite the requirement to avoid it being the key part of the question... This seems to be the only solution for node.js. – carpiediem Dec 12 '18 at 11:33
  • pica also uses canvas, this is not the asked solution though – Pencilcheck Apr 10 '22 at 20:44
  • @Pencilcheck, not it uses bitmap.. but you can create bitmap without canvas, by jpg-js, and then put this bitmap into pica. – Alex Nikulin Apr 17 '22 at 00:22
1

I think this method is best way for this solution.

function base64Resize(sourceBase64, scale , callBack) {

    const _scale = scale;
    var img = document.createElement('img');
    img.setAttribute("src", sourceBase64);

    img.onload = () => {
        var canvas = document.createElement('canvas');
        canvas.width = img.width * _scale;
        canvas.height = img.height * _scale;

        var ctx = canvas.getContext("2d");
        var cw = canvas.width;
        var ch = canvas.height;
        var maxW = img.width * _scale;
        var maxH = img.height * _scale;

        var iw = img.width;
        var ih = img.height;
        var scl = Math.min((maxW / iw), (maxH / ih));
        var iwScaled = iw * scl;
        var ihScaled = ih * scl;
        canvas.width = iwScaled;
        canvas.height = ihScaled;
        ctx.drawImage(img, 0, 0, iwScaled, ihScaled);
        const newBase64 = canvas.toDataURL("image/jpeg", scl);

        callBack(newBase64);
    }
}

Important point is that you should use img.onload event.

Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Mojtaba
  • 39
  • 3
  • 8
    The question says "without using canvas". – Chique Jul 13 '21 at 14:49
  • i'm not familliar with using a callBack - how would I use this? – Michael Fever Aug 16 '21 at 17:48
  • @MichaelFever: when you call the function, you can attach a function as the 3rd parameter which is called once the resizing is done. This can also be an inline anonymous function, e.g. `base64Resize("data:image/png;base64,iVBO...", 0.5, function(resizedImage){ // resizedImage is variable for the resized base64 image });`. But it's not necessary. If you don't want it, remove `callBack` from parameter list and remove the `callBack(newBase64);` line :) – Prid Aug 17 '21 at 00:24
1

My last suggestion (as Chris) was deemed to not answer the question as it used canvas. Because canvas is too obvious.

It's not possible to do without loading the image into memory somewhere so it can be manipulated. Why not just use a library like jimp?

import Jimp from "jimp";

async function resizeBase64Image(base64: string, targetWidth: number, targetHeight: number): Promise<string> {
  // Decode the base64 image data and save it to a buffer
  const imageBuffer = Buffer.from(base64, "base64");

  // Use Jimp to load the image from the buffer and resize it
  const image = await Jimp.read(imageBuffer);
  image.resize(targetWidth, targetHeight);

  // Convert the image back to a base64 data URI
  const resizedImageBase64 = await image.getBase64Async(Jimp.MIME_PNG);

  return resizedImageBase64;
}
Switchur
  • 36
  • 3
0

You may want a resize function that returns the resized data uri:

const resizeBase64Image = (base64: string, width: number, height: number): Promise<string> => {
  // Create a canvas element
  const canvas = document.createElement('canvas') as HTMLCanvasElement;

  // Create an image element from the base64 string
  const image = new Image();
  image.src = base64;

  // Return a Promise that resolves when the image has loaded
  return new Promise((resolve, reject) => {
    image.onload = () => {
      // Calculate the aspect ratio of the image
      const aspectRatio = image.width / image.height;

      // Calculate the best fit dimensions for the canvas
      if (width / height > aspectRatio) {
        canvas.width = height * aspectRatio;
        canvas.height = height;
      } else {
        canvas.width = width;
        canvas.height = width / aspectRatio;
      }

      // Draw the image to the canvas
      canvas.getContext('2d')!.drawImage(image, 0, 0, canvas.width, canvas.height);

      // Resolve the Promise with the resized image as a base64 string
      resolve(canvas.toDataURL());
    };

    image.onerror = reject;
  });
};

And simply await it like this:

const resizedImage: string = await resizeBase64Image(base64, 100, 100);
Chris
  • 113
  • 1
  • 3
  • 3
    Even though it may be a valid working code snippet. You must be attentive to the fact that the question specifically states "without using canvas". Try to add answers only directly relevant to the need of the creator of the post. You can always search through questions which could benefit from this answer. – chri3g91 Dec 20 '22 at 08:35
  • 2
    The answer includes an approach which the creator of the post directly ask not to utilize. – chri3g91 Dec 20 '22 at 08:37
-3
function resizeImage(base64Str) {

      var img = new Image();
      img.src = base64Str;
      var canvas = document.createElement('canvas');
      var MAX_WIDTH = 400;
      var MAX_HEIGHT = 350;
      var width = img.width;
      var height = img.height;

      if (width > height) {
        if (width > MAX_WIDTH) {
          height *= MAX_WIDTH / width;
          width = MAX_WIDTH;
        }
      } else {
        if (height > MAX_HEIGHT) {
          width *= MAX_HEIGHT / height;
          height = MAX_HEIGHT;
        }
      }
      canvas.width = width;
      canvas.height = height;
      var ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0, width, height);
      return canvas.toDataURL();
}

https://gist.github.com/ORESoftware/ba5d03f3e1826dc15d5ad2bcec37f7bf

  • 3
    "without using canvas" is right in the title of the question – miken32 Jan 31 '23 at 02:51
  • 3
    In addition, an answer that's nothing but a block of code is very low quality and likely to be deleted. When answering a question, *especially* an old question, be sure to explain not only how your code answers the question, but also how it improves on the previous **9 years** worth of answers. – miken32 Jan 31 '23 at 02:52
  • While true that this answer uses canvas which was plainly stated as off-limits by OP (as pointed out by @miken32) I must say that this question pops up when googling 'resize base64 image' and to all people who are fine with using canvas (like me), this is a actually quite a fine answer – LukasKroess Jun 21 '23 at 09:44