1

I'm trying to make a subclass of an image library on github called Jimp. As far as I can tell from the docs, you don't instantiate the class in the usual way. Instead of saying new Jimp(), it seems the class has a static method called read that acts as a constructor. From the docs...

Jimp.read("./path/to/image.jpg").then(function (image) {
    // do stuff with the image
}).catch(function (err) {
    // handle an exception
});

It looks like from the docs, that that image returned by read() is an instance allowing the caller to do stuff like image.resize( w, h[, mode] ); and so on.

I'd like to allow my subclass callers to begin with a different static method that reads an image and does a bunch of stuff, summarized as follows...

class MyJimpSubclass extends Jimp {

    static makeAnImageAndDoSomeStuff(params) {
        let image = null;
        // read in a blank image and change it
        return Jimp.read("./lib/base.png").then(_image => {
            console.log(`image is ${_image}`);
            image = _image;
            let foo = image.bar();  // PROBLEM!
            // ...
            // ...
        .then(() => image);
    }

    bar() {
        // an instance method I wish to add to the subclass
    }

// caller
MyJimpSubclass.makeAnImageAndDoSomeStuff(params).then(image => {
    //...
});

You might be able to guess that nodejs gets angry on the line let foo = image.bar();, saying

TypeError image.bar is not a function

.

I think this is understandable, because I got that image using Jimp.read(). Of course that won't return an instance of my subclass.

  1. First idea: Change it to MyJimpSubclass.read(). Same problem.
  2. Second idea: Implement my own static read method. Same problem.

    static read(params) {
        return super.read(params);
    }
    
  3. Third idea: Ask SO

5
  • #2 is a start, but you need to provide your own callback function that converts the Jimp that super.read() returns to the equivalent MyJimpSubclass object. Commented Feb 27, 2018 at 20:29
  • Are you sure that subclassing is what you really need to do, maybe a proxy would be better. Commented Feb 27, 2018 at 20:30
  • @Barmar. Thanks. I'll need to go read about proxy. I want my callers to still be able to use all of the methods that Jimp provides on the resulting image. Commented Feb 27, 2018 at 20:31
  • Yeah, that's generally the right reason for subclassing, and it's not clear if there's an easy way to forward all methods to the contained Jimp without it. Commented Feb 27, 2018 at 20:33
  • have you tried making image MyJimpSubclass member and return an instance of MyJimpSubclass from makeAnImageAndDoSomeStuff. in the caller you access the image via MyJimpSubclass instance. Commented Feb 27, 2018 at 20:35

3 Answers 3

1

The implementation of Jimp.read refers to Jimp specifically, so you would have to copy and change it in your subclass (ick, but not going to break anything since the constructor is also part of the API) or make a pull request to have it changed to this and have subclassing explicitly supported:

static read(src) {
    return new Promise((resolve, reject) => {
        void new this(src, (err, image) => {
            if (err) reject(err);
            else resolve(image);
        });
    });
}

Alternatively, you could just implement all your functionality as a set of functions on a module. This would be next on my list after making a pull request. Would not recommend a proxy.

const makeAnImageAndDoSomeStuff = (params) =>
    Jimp.read("./lib/base.png").then(image => {
        console.log(`image is ${image}`);
        let foo = bar(image);
        // …
        return image;
    });

function bar(image) {
    // …
}

module.exports = {
    makeAnImageAndDoSomeStuff,
    bar,
};

Even changing the prototype would be better than a proxy (but this is just a worse version of the first option, reimplementing read):

static read(src) {
    return super.read(src)
        .then(image => {
            Object.setPrototypeOf(image, this.prototype);
            return image;
        });
}
Sign up to request clarification or add additional context in comments.

4 Comments

Really appreciate the help. I have no intention of merging back into the github (or even updating from there further). So as icky as it might seem, I'm going with your idea #3.
@user1272965: You sure about not going with #1? It’s the same idea (both override read, both use the subclass prototype with new this/this.prototype), but uses a documented constructor instead of a hack.
I'll check it again. In my first attempt it failed (I thought, because I never saw where it invoked super read), but maybe I had a problem elsewhere. I'll also go look at the source for constructor in the Jimp library, which I should have done anyway. I read someplace that you're not supposed to say "thank you" in SO comments, but thank you!
@user1272965: Happy to help! I think it should work (only the constructor needs to call the super constructor – static methods don’t have this requirement), but if you run into a problem again I’d be interested in the exact error message.
1

You have a couple of options. The cleanest is probably to make a subclass like you started, but then implement the Jimp static method on it, as well as your own. In this case, it's not really inheritance, so don't use extends.

class MyJimp {
  static read(...args) {
    return Jimp.read.apply(Jimp, args);
  }

  static makeAnImage(params) {
    return this.read(params)
      .then(image => {
        // do stuff
        return image
      });
  }
}

From there, I would make an object which has all of the new functions you want to apply to image:

const JimpImageExtension = {
  bar: () => { /* do something */ }
};

Finally, in your static methods, get the image and use Object.assign() to apply your new functions to it:

class MyJimp {
  static read(...args) {
    return Jimp.read.apply(Jimp, args)
      .then(image => Object.assign(image, JimpImageExtension));
  }

  static makeAnImage(params) {
    return this.read(params)
      .then(image => {
        // do stuff
        image.bar();
        return image;
      });
  }
}

This should do the trick by applying your extra functions to the image. You just need to make sure that you apply it at every point that can generate an image (if there is more than just read). Since in the other functions, it's using your version of read(), you only need to add the functions in the one.

Another approach would be if Jimp makes their image class accessible, you could also add them to the prototype of that (though usually in libraries like this, that class is frequently inaccessible or not actually a class at all).

1 Comment

Thanks for this very thoughtful reply. I may end up switching to it. Will return here if I do.
1

This might be a way to do it. Start with your own read method, and have it change the prototype of the returned object.

static read(...params) {
    return super.read(...params).then(image) {
        image.prototype = MyJimpSubclass;
        resolve(image);
    }
}

9 Comments

You mean image.__proto__ instead of image.prototype, super.read(...params) instead of super.read.apply(super, params), MyJimpSubclass.prototype instead of MyJimpSubclass, and return instead of resolve?
Really appreciate all the smart people helping me here. I'm getting caught on the super parameter to apply. "SyntaxError: 'super' keyword unexpected here"
I don't think super can be used as a variable as I tried in the apply function. I've changed to using the spread operator.
ok. thanks, and is it furthermore your idea answer a new promise? Is that how I should interpret "resolve(image)" ?
Yes, that's my intent. I haven't done much promise programming, I hope I got it right.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.