3

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.

1 Answers1

3

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

Swiffy
  • 4,401
  • 2
  • 23
  • 49
  • Thank you so much for your answer Swiffy, what an effort. Definitely worth the bounty. I have one requested addition - as it took me a while to work out after employing your solution. I was wanting to colorise the images with the array (in the original code) to iterate through all the colours and create every possible colour combination. But when I went to do it, the "operations" params wouldn't take the string colors I had or an array of hex colors (like you had), it had to be an array of RGBA in this format {r: 255, g: 255, b: 255, a: 255). It might be helpful to others to add this detail. – James Stephen Brown Jun 20 '22 at 06:54
  • Yep! You have to fiddle a bit for that. There is no `{ apply: "purple", params: [255] }` - that only supports red, green, blue. If you really want to generate an image for every rgb-color there is (16,777,216 of them), the easiest way would probably be to generate all of the almost 17 million operations required with different rgb values like: `let ops = [[{ apply: 'mix', params: [rgb1, 100] }], [{ apply: 'mix', params: [rgb2, 100] }]]`. 1/2 – Swiffy Jun 20 '22 at 10:17
  • Mix, tint and shade are awfully documented, [but this is how mix is implemented](https://i.imgur.com/OJYFm6H.png), where clr is some pixel's current color and it is called internally like this `clr = mix(clr, action.params[0], action.params[1]);`. Tint and shade are also just mix, but they are called like `clr = mix(clr, { r: 255, g: 255, b: 255 }, action.params[0]);` and `clr = mix(clr, { r: 0, g: 0, b: 0 }, action.params[0]);`. – Swiffy Jun 20 '22 at 10:19
  • Ha, wasn't looking for 17 million, just the 6 colors in the array :) I just replaced the color array with 6 rgba{...} colors, and it worked. I just thought that would improve the answer if you could include that part of the process, as it was a part of the question. But I'm happy to accept the answer as is, if you feel you've done enough :) – James Stephen Brown Jun 21 '22 at 01:45
  • Could you give me the rgba values? – Swiffy Jun 21 '22 at 05:05
  • 1
    Oh yes, sorry. I didn't mean you had to put in specific values, but I ended up with this array. 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: 137, g: 91, b: 255, a: 1}, {r: 255, g: 190, b: 171, a: 1}, {r: 255, g: 239, b: 117, a: 1} ]; – James Stephen Brown Jun 22 '22 at 06:16
  • I then looped though the colours and entered the params like: { apply: 'mix', params: [colors[index], 60] }, 60 was a good value for a strong color while retaining the tonal range in the picture. – James Stephen Brown Jun 22 '22 at 06:20
  • Do those correspond to your initial color array - green, red, ..., orange? Might as well make it match the question – Swiffy Jun 22 '22 at 06:29
  • 1
    This one is in order :) 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} ]; – James Stephen Brown Jun 22 '22 at 06:36
  • So, just to be clear, I only used on "mix" params line in the operations function - but looped through each picture so from 5 pictures you get 30 pictures output each picture colorised with each color. – James Stephen Brown Jun 22 '22 at 06:39
  • Edited my answer to match. – Swiffy Jun 22 '22 at 07:15