javascriptdimensionsjimpcolorize

Colorizing an image using jimp


I am coloring png images from a folder using jimp, but I'm getting the error: w and h must be numbers (Line 42 - the image.color function.). This seems like it should be a simple operation but solutions I have found have been extremely complicated. It seems as though jimp is the way to go but obviously it has some quirks I'm not familiar with.

const { jimpEvChange } = require('@jimp/core');
const { write } = require('jimp');
const { composite } = require('jimp');
const jimp = require('jimp');
var fs = require('fs');

// create an array of 6 colors and specify the colors
const colors = [
    ['green'],
    ['red'],
    ['blue'],
    ['yellow'],
    ['purple'],
    ['orange']
];

// call functions to colorize the images
var pngFiles = GetPNGs("ToColor/");
for (var i = 0; i < pngFiles.length; i++) {
    var image = new jimp(pngFiles[i]);
    Colorize(image, colors[i]);
    image.write(pngFiles[i]);
}


// get pngs from a folder "ToColor" and colorize them each using the colors array
function GetPNGs (folder) {
    var pngFiles = [];
    const newLocal = fs.readdirSync(folder);
    var files = newLocal;
    for (var i = 0; i < files.length; i++) {
        var file = files[i];
        if (file.split(".").pop() == "png") {
            pngFiles.push(folder + "/" + file);
        }
    }
    return pngFiles;
}

// colorize the images
function Colorize (image, color) {
    image.color([
        { apply: 'red', params: [color[0]] },
        { apply: 'green', params: [color[0]] },
        { apply: 'blue', params: [color[0]] }
    ]);
}

// loop through the images and colorize them
function ColorizeImages (pngs, colors) {
    for (var i = 0; i < pngs.length; i++) {
        var image = new jimp(pngs[i]);
        Colorize(image, colors[i]);
        image.write(pngs[i]);
    }
}

Any tips would be much appreciated. Thanks, James.


Solution

  • Allright I took a crack at it and came up with this example:

    Note that this code needs to be in a file with .mjs extension, because we're using import statements instead of require. You can run .mjs file exactly the same way than normal .js files with node index.mjs. If you really want to use requires instead, change the imports to requires and name the file normally with .js extension.

    import jimp from "jimp";
    import fs from "fs";
    
    // I wanted to make this example to use async/await properly with Jimp
    // So that's why we are using util.promisify to convert fs.readdir
    // into a function named readDir, which we can await on
    import util from "util";
    const readDir = util.promisify(fs.readdir);
    
    // Colors for mix operations
    const colors = [ 
        {r: 0, g: 255, b: 154, a: 1}, 
        {r: 255, g: 40, b: 108, a: 1}, 
        {r: 26, g: 172, b: 255, a: 1}, 
        {r: 255, g: 190, b: 171, a: 1}, 
        {r: 255, g: 239, b: 117, a: 1}, 
        {r: 137, g: 91, b: 255, a: 1} 
    ];
    
    // Colorsnames for output file naming, these correspond to colors array
    const colorNames = ['green', 'red', 'blue', 'orange', 'yellow', 'purple'];
    
    // Define which color operations we want to do, using mix as an example
    // https://www.npmjs.com/package/jimp#colour-manipulation
    const operations = colors.map((c) => {
        return { apply: "mix", params: [c, 60 ]};
    });
    
    // Input and output folder names
    const inputFolderName = "./ToColor";
    const outputolderName = "./out";
    const outputFileSuffix = "edited"; // Optional suffix for the output files
    
    // We're using async/await, so must wrap top level code like this
    // https://stackoverflow.com/questions/46515764/how-can-i-use-async-await-at-the-top-level
    (async () => {
    
        // Get filenames of the png files in the specified folder
        let pngFileNames = await readDir(inputFolderName);
    
        // Optional filtering of only .png files
        pngFileNames = pngFileNames.filter((f) => f.includes(".png"));
    
        // Go through each file
        // Must use for...of loop here, because we have awaits inside the loop
        let i = 0;
        for (let fileName of pngFileNames) {
    
            // Optional output file name suffixing
            const outPutFileName = outputFileSuffix.length > 0 ? fileName.split('.').reduce((a, b) => `${a}_${outputFileSuffix}.${b}`) : fileName;
    
            // Make an actual Jimp image object from the file
            const jimpImage = await jimp.read(`${inputFolderName}/${fileName}`);
    
            // Make one new image per operation, so in total, we output colors.length * pngFileNames.length images
            let j = 0;
            for(let colorOperation of operations) {
                // Apply operation
                jimpImage.color([colorOperation]);
    
                // Write the edited image to out folder
                await jimpImage.writeAsync(`${outputolderName}/${colorNames[j]}_${outPutFileName}`);
                j++;
            }
            
            i++;
        }
        
    })();
    

    Your code had, well, multiple problems. There was some issues regarding reading the actual images and a multitude of issues regarding using the Jimp library, but I am not going to go through all of them unless you want me to.

    You are right though about the Jimp documentations, it's... awful. Especially if you are somewhat rookie with JavaScript in general.

    You biggest issue was probably how you tried to create new Jimp image objects. The documentation says that using new Jimp(...) is for creating new images, which means that you would use it if you had no images anywhere in the first place.

    However, when you already have your images in some folder and want to load them up to edit with Jimp, you need to use jimp.read(...) instead. jimp.read is an asynchronous function, which means that the rest of your code will continue running even if the image hasn't been read yet. For this reason we need to use await jimp.read which you could think of like "pausing" the program until jimp.read has actually read the image.

    After the image has been read and the image object lies into a variable named jimpImage, we call jimpImage.color() with the array of predefined operations, in this case we're using mix. This function is not asynchronous, so we don't have to await it.

    Finally after we've applied the coloring operations to the image, we save the image to the specified output folder with the same name (and optional suffix) by using writeAsync. This is an asynchronous function as the name of it implies, so we have to await it.

    After the program has finished running, you can find your modified images in the specified output folder.

    Also note that Jimp delegates some of the documentation, especially regarding "color stuff", to TinyColor Github page, as Jimp uses TinyColor under the hood for certain color related utility stuff. So if you're wondering if you can use the word "red" instead of "#FF0000" for example, TinyColor documentation has the answer for that.

    Regarding the error: w and h must be numbers-error; most likely cause for it was that you initialized the images wrong for Jimp with var image = new jimp(pngFiles[i]);. Like I said, this is for creating new images from scratch and I refer to the documentation again, which says that if you ARE using this syntax to create new images, it is used like this (where the first two parameters are the width and height, which were not given in your code):

    new Jimp(256, 256, (err, image) => {
      // this image is 256 x 256, every pixel is set to 0x00000000
    });
    

    I've given you a simplified example of how to read images, apply some operations to them and write the modified images back to some folder. I'll leave the rest to you!

    Do ask if you have any questions, I'm a Jimp master now.

    These are the test images I used:

    testimgs

    And these are what the program output (remember the amount is only 60 and our base images have strong colors):

    output