asilvas / node-image-steam

A simple, fast, and highly customizable on-the-fly image manipulation web server built atop Node.js

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to use Sharp's contant-based config values

OktarinTentakel opened this issue · comments

Hi,

I've been looking at the Sharp resize fit and position options you linked in the documentation and saw, that sharp includes things like "sharp.strategy.entropy" and "sharp.strategy.attention", which are really cool features. But reading further in the examples I saw that those features are actually triggered by using imported constants in Node rather than strings which seems to clash with imagesteam's definition of operations.

I then had a look at the helper source for resize and saw that the provided values are used verbatim (as strings) and there does not seem to be any mapping to these (new?) operations.

Is there any ways to use these constant-based Sharp setups in imagesteam and how would I do that?

Thanks a lot.

BTW: awesome package

howdy @OktarinTentakel. Currently no way to do this, but the changes should be very minimal. Happy to take a PR.

Here's where you would add the new mapping for strategy: https://github.com/asilvas/node-image-steam/blob/master/lib/router/router-defaults.js#L47

Then supply to the resize: https://github.com/asilvas/node-image-steam/blob/master/lib/processor/steps/resize.js#L105

It could look something like /my-image.jpg/:/rs=s:entropy

Couldn't you just add a wrapper to resolve sharp paths dynamically? In that case you'd match the API directly and would never have to touch the mappings again.

Someting like this:

function resolveSharpConstant(sharp, optionValue){
	const stringOptionValue = `${optionValue}`;

	if( !stringOptionValue.startsWith('sharp.') ) return optionValue;

	let sharpConstant = sharp;
	stringOptionValue.split('.').slice(1).forEach(pathPart => {
		if( sharpConstant !== undefined ){
			sharpConstant = sharpConstant[pathPart];
		}
	});

	if( sharpConstant === undefined ){
		return optionValue;
	} else {
		return sharpConstant;
	}
}

function resolveSharpConstants(sharp, config){
	for( const [key, value] of Object.entries(config) ){
		config[key] = resolveSharpConstant(sharp, value);
	}

	return config;
}

context.sharp
    .resize(stepInfo.width, stepInfo.height, resolveSharpConstants(context.sharp, {
      interpolator: stepInfo.interpolator || 'bicubic',
      fit: stepInfo.fit || 'fill',
      position: stepInfo.position || 'centre',
      background: rgba
    }))
;

Not a bad idea, but feels more like a solution looking for a problem at this point. I'd like to keep the scope down to the topic in question.

I'll put some more thought into this topic after the holidays. It would likely require a fair amount of testing, and the return is a little questionable IMO.

Don't think so, actually. That's just an extension of what you are currently doing.

Currently you are providing Sharp with the given values "as is" and rely on the public API and the sane config of the user.

This does exactly that without changing the general process, the only difference being that path-defined values are resolved to internal constants if they have the expected prefix, thereby matching a formatted string to its internal constant-representation. If you want to restrict this a little more, you could add allowed namespaces after "sharp." to the resolver, but I don't think this is really necessary. If the user somehow configures a value that maps to an unusable constant it's the same error situation which would arise if Sharp gets any other nonsense value, which should be covered already.

And why is this testing-intensive in your opinion? You are just piping through config values to the Sharp renderer, which is exactly what you've been doing the whole time. The two interesting points I'm seeing here are performance and stability of the new functions and what actually happens if you manage to sneak in a bullshit constant and how error handling reacts to that.

But on the other hand: you're already relying on Sharp as the implementation layer performance-wise anyway and are not introducing anything obligatory here and error handling is present as well, wrapping the rendering, so this is rather a question of "does it work in this case as well".

And the return would simply be, that the full functionality of Sharp would be available, while this is currently restricted by the option datatype having to be a simple non-constant value, which currently defines a subset, leaving out the last additions an more in-depth concepts.

Happy to take a PR. A couple requests:

  • No need for the sharp. prefix.
  • The helper should infer the data type (ala "false" should converted to boolean, "123" should be a number, everything else keep as string).

It's not clear to me from your code how exactly you're resolving the values, but I'm fine to review a working PR with tests.

I'm resolving the constants by walking the dot-path of the attribute's value, starting with context.sharp, which is the sharp instance you are using and after that simply looking for the symbol named like the part part while going down, that way you are mapping each dot-path-part step by step while resolving each step going deeper into the sharp instance.

let sharpConstant = sharp;
stringOptionValue.split('.').slice(1).forEach(pathPart => {
    if( sharpConstant !== undefined ){
        sharpConstant = sharpConstant[pathPart];
    }
});

And why should we do anything to constant types? The values are set by Sharp and they are resolved by unique names, there are no user definable values here. Currently there is no type casting going on and especially when using constants, there should not be any need to do that. I'm confused. 🤔

Edit: The sharp.prefix (or any prefix) is necessary to allow for constants on first level, which otherwise would be indistinguishable from simple string values.