2

I have an image in the form of a buffer in NODEjs, and I want to resize it.

Theoretically this should be able to be done in nodeJS, since I have access to the buffer, which contains all of the pixel data.

I have looked many places to find a simple way to resize an image using NATIVE (only!) nodejs, with no external libraries, but I have only found solutions that use libraries: https://www.npmjs.com/package/gm

Node gm - resize image and preserve aspect ratio?

Node.js: image resizing without ImageMagick Easy way to resize image in Node.js?

How to resize an image in Node.js?

How to resize image in Node js

Node.js Resize Image

Resize image in node js

Download image and resize in nodejs

Resizing images with Nodejs and Imagemagick

Resize image to exact size by maintaining aspect ratio nodejs

How to resize images on node.js

Resize and crop image and keeping aspect ratio NodeJS & gm

How to resize image size in nodejs using multer

try to resize the stream image with sharp Node.js

Node.js: image resizing without ImageMagick

resize an image without uploading it to anywhere using gm in nodejs

Resize an image in Node.js using jimp and get the path the new image

but ALL of these solutions use some kind of library, but I want to only use plain NodeJS.

I can read the pixels with a Buffer, so I should be able to write a resized Buffer, like this C++ thread http://www.cplusplus.com/forum/general/2615/ and many others, that simply loop through the pixels and resize it..

I found this question Resizing an image in an HTML5 canvas which implemented image resizing with pure client-side JavaScript, without depending on the canvas drawImage to resize it (only to get the image data), this was the code he used:

function lanczosCreate(lobes) {
    return function(x) {
        if (x > lobes)
            return 0;
        x *= Math.PI;
        if (Math.abs(x) < 1e-16)
            return 1;
        var xx = x / lobes;
        return Math.sin(x) * Math.sin(xx) / x / xx;
    };
}

// elem: canvas element, img: image element, sx: scaled width, lobes: kernel radius
function thumbnailer(elem, img, sx, lobes) {
    this.canvas = elem;
    elem.width = img.width;
    elem.height = img.height;
    elem.style.display = "none";
    this.ctx = elem.getContext("2d");
    this.ctx.drawImage(img, 0, 0);
    this.img = img;
    this.src = this.ctx.getImageData(0, 0, img.width, img.height);
    this.dest = {
        width : sx,
        height : Math.round(img.height * sx / img.width),
    };
    this.dest.data = new Array(this.dest.width * this.dest.height * 3);
    this.lanczos = lanczosCreate(lobes);
    this.ratio = img.width / sx;
    this.rcp_ratio = 2 / this.ratio;
    this.range2 = Math.ceil(this.ratio * lobes / 2);
    this.cacheLanc = {};
    this.center = {};
    this.icenter = {};
    setTimeout(this.process1, 0, this, 0);
}

thumbnailer.prototype.process1 = function(self, u) {
    self.center.x = (u + 0.5) * self.ratio;
    self.icenter.x = Math.floor(self.center.x);
    for (var v = 0; v < self.dest.height; v++) {
        self.center.y = (v + 0.5) * self.ratio;
        self.icenter.y = Math.floor(self.center.y);
        var a, r, g, b;
        a = r = g = b = 0;
        for (var i = self.icenter.x - self.range2; i <= self.icenter.x + self.range2; i++) {
            if (i < 0 || i >= self.src.width)
                continue;
            var f_x = Math.floor(1000 * Math.abs(i - self.center.x));
            if (!self.cacheLanc[f_x])
                self.cacheLanc[f_x] = {};
            for (var j = self.icenter.y - self.range2; j <= self.icenter.y + self.range2; j++) {
                if (j < 0 || j >= self.src.height)
                    continue;
                var f_y = Math.floor(1000 * Math.abs(j - self.center.y));
                if (self.cacheLanc[f_x][f_y] == undefined)
                    self.cacheLanc[f_x][f_y] = self.lanczos(Math.sqrt(Math.pow(f_x * self.rcp_ratio, 2)
                            + Math.pow(f_y * self.rcp_ratio, 2)) / 1000);
                weight = self.cacheLanc[f_x][f_y];
                if (weight > 0) {
                    var idx = (j * self.src.width + i) * 4;
                    a += weight;
                    r += weight * self.src.data[idx];
                    g += weight * self.src.data[idx + 1];
                    b += weight * self.src.data[idx + 2];
                }
            }
        }
        var idx = (v * self.dest.width + u) * 3;
        self.dest.data[idx] = r / a;
        self.dest.data[idx + 1] = g / a;
        self.dest.data[idx + 2] = b / a;
    }

    if (++u < self.dest.width)
        setTimeout(self.process1, 0, self, u);
    else
        setTimeout(self.process2, 0, self);
};
thumbnailer.prototype.process2 = function(self) {
    self.canvas.width = self.dest.width;
    self.canvas.height = self.dest.height;
    self.ctx.drawImage(self.img, 0, 0, self.dest.width, self.dest.height);
    self.src = self.ctx.getImageData(0, 0, self.dest.width, self.dest.height);
    var idx, idx2;
    for (var i = 0; i < self.dest.width; i++) {
        for (var j = 0; j < self.dest.height; j++) {
            idx = (j * self.dest.width + i) * 3;
            idx2 = (j * self.dest.width + i) * 4;
            self.src.data[idx2] = self.dest.data[idx];
            self.src.data[idx2 + 1] = self.dest.data[idx + 1];
            self.src.data[idx2 + 2] = self.dest.data[idx + 2];
        }
    }
    self.ctx.putImageData(self.src, 0, 0);
    self.canvas.style.display = "block";
};

and then for an image (made with var img = new Image(); img.src = "something"):

img.onload = function() {
    var canvas = document.createElement("canvas");
    new thumbnailer(canvas, img, 188, 3); //this produces lanczos3
    // but feel free to raise it up to 8. Your client will appreciate
    // that the program makes full use of his machine.
    document.body.appendChild(canvas);
};

So first of all, this is incredibly slow on the client side, but still maybe it could be faster on the server. Things that need to be replaced / don't exist in node is ctx.getImageData (which could possibly be replicated with buffers)

does anyone know where to start with this in nodejs, and is this at all practical performance wise? If not can this be made with pure node-gyp to increase performance, using the above mentioned C++ tutorial's code? (which is the following):

#include<iostream>

class RawBitMap
{
public:
    RawBitMap():_data(NULL), _width(0),_height(0)
    {};

    bool Initialise()
    {
        // Set a basic 2 by 2 bitmap for testing.
        //
        if(_data != NULL)
            delete[] _data;

        _width = 2;
        _height = 2;    
        _data = new unsigned char[ GetByteCount() ];

        //
        _data[0] = 0;   // Pixels(0,0) red value
        _data[1] = 1;   // Pixels(0,0) green value
        _data[2] = 2;   // Pixels(0,0) blue value
        _data[3] = 253; // Pixels(1,0)
        _data[4] = 254;
        _data[5] = 255;
        _data[6] = 253; // Pixels(0,1)
        _data[7] = 254;
        _data[8] = 255;
        _data[9] = 0;   // Pixels(1,1)
        _data[10] = 1;
        _data[11] = 2;  

        return true;
    }

    // Perform a basic 'pixel' enlarging resample.
    bool Resample(int newWidth, int newHeight)
    {
        if(_data == NULL) return false;
        //
        // Get a new buuffer to interpolate into
        unsigned char* newData = new unsigned char [newWidth * newHeight * 3];

        double scaleWidth =  (double)newWidth / (double)_width;
        double scaleHeight = (double)newHeight / (double)_height;

        for(int cy = 0; cy < newHeight; cy++)
        {
            for(int cx = 0; cx < newWidth; cx++)
            {
                int pixel = (cy * (newWidth *3)) + (cx*3);
                int nearestMatch =  (((int)(cy / scaleHeight) * (_width *3)) + ((int)(cx / scaleWidth) *3) );

                newData[pixel    ] =  _data[nearestMatch    ];
                newData[pixel + 1] =  _data[nearestMatch + 1];
                newData[pixel + 2] =  _data[nearestMatch + 2];
            }
        }

        //
        delete[] _data;
        _data = newData;
        _width = newWidth;
        _height = newHeight; 

        return true;
    }

    // Show the values of the Bitmap for demo.
    void ShowData()
    {
        std::cout << "Bitmap data:" << std::endl;
        std::cout << "============" << std::endl;
        std::cout << "Width:  " << _width  << std::endl;
        std::cout << "Height: " << _height  << std::endl;
        std::cout << "Data:" << std::endl;

        for(int cy = 0; cy < _height; cy++)
        {
            for(int cx = 0; cx < _width; cx++)
            {
                int pixel = (cy * (_width *3)) + (cx*3);
                std::cout << "rgb(" << (int)_data[pixel] << "," << (int)_data[pixel+1] << "," << (int)_data[pixel+2] << ") ";
            }
            std::cout << std::endl;
        }
        std::cout << "_________________________________________________________" << std::endl;
    }


    // Return the total number of bytes in the Bitmap.
    inline int GetByteCount()
    {
        return (_width * _height * 3);
    }

private:
    int _width;
    int _height;
    unsigned char* _data;

};


int main(int argc, char* argv[])
{
    RawBitMap bitMap;

    bitMap.Initialise();
    bitMap.ShowData();

    if (!bitMap.Resample(4,4))
        std::cout << "Failed to resample bitMap:" << std::endl ; 
    bitMap.ShowData();

    bitMap.Initialise();
    if (!bitMap.Resample(3,3))
        std::cout << "Failed to resample bitMap:" << std::endl ;
    bitMap.ShowData();


    return 0;
}

I guess this is creating a 2x2 bitmap and resizing it, but still the basic principle should be able to be applied with pure node-gyp. Has anyone else done this? Is this at all practical?

bluejayke
  • 2,773
  • 2
  • 20
  • 50
  • 1
    Did you see jimp? https://github.com/oliver-moran/jimp It's a pure javascript image processing library, and includes a high quality resize. It will be a lot slower than native solutions, of course, perhaps 40x slower, see https://sharp.pixelplumbing.com/performance though of course performance is not the only factor when selecting a library. – jcupitt Feb 01 '20 at 04:51
  • @jcupitt oh cool, I actually found jimp in other answers, but instnatly dismissed it, not realizing it was entirely written in javascript, thanks for pointing it out – bluejayke Feb 02 '20 at 23:37

1 Answers1

1

Found an easy, fast method, using only the pngjs library for node (which is written in pure native node, so even that could be optimized), and the built in stream library. So at the top of your code just do var PNG = require("pngjs").PNG, stream = require("stream"); then use the following Code:

function cobRes(iBuf, width, cb) {
        b2s(iBuf)
        .pipe(new PNG({
            filterType: -1
        }))
        .on('parsed', function() {

            var nw = width;
            var nh = nw *  this.height /this.width;
            var f = resize(this, nw, nh);

            sbuff(f.pack(), b=>{
                cb(b);
            })
        })


        function resize(srcPng, width, height) {
            var rez = new PNG({
                width:width,
                height:height
            });
            for(var i = 0; i < width; i++) {
                var tx = i / width,
                    ssx = Math.floor(tx * srcPng.width);
                for(var j = 0; j < height; j++) {
                    var ty = j / height,
                        ssy = Math.floor(ty * srcPng.height);
                    var indexO = (ssx + srcPng.width * ssy) * 4,
                        indexC = (i + width * j) * 4,
                        rgbaO = [
                            srcPng.data[indexO  ],
                            srcPng.data[indexO+1],
                            srcPng.data[indexO+2],
                            srcPng.data[indexO+3]
                        ]
                    rez.data[indexC  ] = rgbaO[0];
                    rez.data[indexC+1] = rgbaO[1];
                    rez.data[indexC+2] = rgbaO[2];
                    rez.data[indexC+3] = rgbaO[3];
                }
            }
            return rez;
        }

        function b2s(b) {
            var str = new stream.Readable();
            str.push(b);
            str.push(null);
            return str;
        }
        function sbuff(stream, cb) {
            var bufs = []
            var pk = stream;
            pk.on('data', (d)=> {
                bufs.push(d);

            })
            pk.on('end', () => {
                var buff = Buffer.concat(bufs);
                cb(buff);
            });
        }
    }

then to use:

cobRes(fs.readFileSync("somePNGfile.png"), 200, buffer => fs.writeFileSync("new.png", buffer))

not sure why everyone's using complicated libraries for this:)

bluejayke
  • 2,773
  • 2
  • 20
  • 50
  • 1
    This is nice and simple, but it'll be slow and the quality won't be great, since you are doing nearest-neighbour interpolation. You'll get ugly jaggies on edges and nasty moire effects on repeating patterns. https://stackoverflow.com/a/57678066/894763 has a test image plus samples to show the kinds of artifacts you can get. – jcupitt Feb 26 '20 at 08:22
  • @jcupitt interesting, do you have an alternative with native JS only? – bluejayke Feb 26 '20 at 20:53
  • Hello, yes, jimp has a high-quality resize operation, eg. https://github.com/oliver-moran/jimp/blob/master/packages/plugin-resize/src/modules/resize2.js#L252 it will be slow though, perhaps 40x slower than a native resize of equivalent quality. – jcupitt Feb 27 '20 at 08:16