Skip to content

Instantly share code, notes, and snippets.

@farukcankaya
Last active April 18, 2022 09:29
Show Gist options
  • Save farukcankaya/5b698044d907fe80a7c1f6e3afe618bc to your computer and use it in GitHub Desktop.
Save farukcankaya/5b698044d907fe80a7c1f6e3afe618bc to your computer and use it in GitHub Desktop.
Using custom/complex imagemagick command with GraphicsMagick for node
"use strict";
const ImageData = require("./ImageData");
const ImageConverterOperator = require("./ImageConverterOperator");
const gm = require("gm").subClass({imageMagick: true})
/**
* Get enable to use memory size in ImageMagick
* Typically we determine to us 90% of max memory size
* @see https://docs.aws.amazon.com/lambda/latest/dg/lambda-environment-variables.html
*/
const getEnableMemory = () => {
const mem = parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, 10);
return Math.floor(mem * 90 / 100);
};
class ImageConverter {
/**
* Image Converter
* convert image with ImageMagick
*
* @constructor
* @param String parameters
*/
constructor(parameters) {
this.parameters = parameters;
}
/**
* Execute convert
*
* @public
* @param ImageData image
* @return Promise
*/
exec(image) {
return new Promise((resolve, reject) => {
console.log("Converting...");
let img = gm(image.data).limit("memory", `${getEnableMemory()}MB`);
let convert = img.command("convert");
let operationAndParameters = this.parseArgsStringToArgv(this.parameters);
let operatorIndexes = this.findIndexOfOperators(operationAndParameters);
let operators = this.generateOperators(operatorIndexes, operationAndParameters);
operators.forEach(o => convert.out(o.operator, ...o.parameters));
// @see: https://github.com/aheckmann/gm/issues/572#issuecomment-293768810
img.stream((err, stdout, stderr) => {
if (err) {
return reject(err);
}
const chunks = [];
stdout.on('data', (chunk) => {
chunks.push(chunk);
});
// these are 'once' because they can and do fire multiple times for multiple errors,
// but this is a promise so you'll have to deal with them one at a time
stdout.once('end', () => {
resolve(Buffer.concat(chunks));
});
stderr.once('data', (data) => {
reject(String(data));
});
});
});
}
generateOperators(operatorIndexes, operationAndParameters){
let operators = [];
for (const [i, operatorIndex] of operatorIndexes.entries()) {
const nextOperatorIndex = operatorIndexes[i+1];
let operator = operationAndParameters[operatorIndex];
let parameters = operationAndParameters.slice(operatorIndex+1, nextOperatorIndex);
operators.push(new ImageConverterOperator(operator, parameters));
}
return operators;
}
findIndexOfOperators(operationAndParameters){
let indexes = []
for (const [index, operator] of operationAndParameters.entries()) {
if(operator.startsWith("-") || operator.startsWith("+")) {
indexes.push(index);
}
}
return indexes;
}
// Exact copy of https://github.com/mccormicka/string-argv
parseArgsStringToArgv(value, env, file) {
// ([^\s'"]([^\s'"]*(['"])([^\3]*?)\3)+[^\s'"]*) Matches nested quotes until the first space outside of quotes
// [^\s'"]+ or Match if not a space ' or "
// (['"])([^\5]*?)\5 or Match "quoted text" without quotes
// `\3` and `\5` are a backreference to the quote style (' or ") captured
var myRegexp = /([^\s'"]([^\s'"]*(['"])([^\3]*?)\3)+[^\s'"]*)|[^\s'"]+|(['"])([^\5]*?)\5/gi;
var myString = value;
var myArray = [];
if (env) {
myArray.push(env);
}
if (file) {
myArray.push(file);
}
var match;
do {
// Each call to exec returns the next regex match as an array
match = myRegexp.exec(myString);
if (match !== null) {
// Index 1 in the array is the captured group if it exists
// Index 0 is the matched text, which we use if no captured group exists
myArray.push(this.firstString(match[1], match[6], match[0]));
}
} while (match !== null);
return myArray;
}
// Exact copy of https://github.com/mccormicka/string-argv
// Accepts any number of arguments, and returns the first one that is a string
// (even an empty string)
firstString() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (typeof arg === "string") {
return arg;
}
}
}
}
module.exports = ImageConverter;
class ImageConverterOperator {
constructor (operator, parameters) {
this.operator = operator
this.parameters = parameters
this.name = this.constructor.name
}
}
module.exports = ImageConverterOperator
/**
GraphicsMagic supports customer operators like https://github.com/aheckmann/gm/issues/655#issuecomment-411481724
However, it is very cumbersome to add every operator that imagemagic has.
This is a converter to automate generating GM operators from given imagemagic `convert` command.
Example:
Let's say you want to run the imagemagick's command below with GraphicsMagic for Node:
```shell
convert (-clone 0 -background "black" -shadow "80x3+10+10") \
(-clone 0 -background "white" -shadow "80x3+-5-5") -reverse \
-background "none" -layers "merge" +repage`
```
GraphicMagic for node does not have an operator for it but you can build your operator
using .in() and .out() operators of GM. Like:
```nodejs
gm(fs.readFileSync(path))
.command("convert")
.out("(")
.out("-clone", "0")
.out("-background", "black")
.out("-shadow", "80x3+10+10")
.out(")")
.out("(")
.out("-clone", "0")
.out("-background", "white")
.out("-shadow", "80x3+-5-5")
.out(")")
.out("-reverse")
.out("-background", "none")
.out("-layers", "merge")
.out("+repage")
.toBuffer('PNG', function(err, buffer) {
console.log('test-gm', err, buffer);
if(!err) {
fs.writeFileSync(outPath, buffer);
console.log('Written', outPath);
}
});
```
You can generate GM operator chain using the following script!
*/
// Given query
let COMPLEX_IMAGEMAGIC_CONVERT_QUERY = "(-clone 0 -background \"black\" -shadow \"80x3+10+10\") (-clone 0 -background \"white\" -shadow \"80x3+-5-5\") -reverse -background \"none\" -layers \"merge\" +repage";
// Initiate GM
let img = gm(image).limit("memory", `${getEnableMemory()}MB`);
let imageToConvert = img.command("convert");
// Generate operators
let operationAndParameters = parseArgsStringToArgv(COMPLEX_IMAGEMAGIC_CONVERT_QUERY);
let operatorIndexes = findIndexOfOperators(operationAndParameters);
let operators = generateOperators(operatorIndexes, operationAndParameters);
// Apply operators to the image dynamically
operators.forEach(o => imageToConvert.out(o.operator, ...o.parameters));
// That's it! Then you can stream your image:
img.stream((err, stdout, stderr) => {
...
});
// Used Methods and Classes ===========================================================================================>
class ImageConverterOperator {
constructor (operator, parameters) {
this.operator = operator
this.parameters = parameters
this.name = this.constructor.name
}
}
generateOperators(operatorIndexes, operationAndParameters){
let operators = [];
for (const [i, operatorIndex] of operatorIndexes.entries()) {
const nextOperatorIndex = operatorIndexes[i+1];
let operator = operationAndParameters[operatorIndex];
let parameters = operationAndParameters.slice(operatorIndex+1, nextOperatorIndex);
operators.push(new ImageConverterOperator(operator, parameters));
}
return operators;
}
findIndexOfOperators(operationAndParameters){
let indexes = []
for (const [index, operator] of operationAndParameters.entries()) {
if(operator.startsWith("-") || operator.startsWith("+")) {
indexes.push(index);
}
}
return indexes;
}
// Exact copy of https://github.com/mccormicka/string-argv
parseArgsStringToArgv(value, env, file) {
// ([^\s'"]([^\s'"]*(['"])([^\3]*?)\3)+[^\s'"]*) Matches nested quotes until the first space outside of quotes
// [^\s'"]+ or Match if not a space ' or "
// (['"])([^\5]*?)\5 or Match "quoted text" without quotes
// `\3` and `\5` are a backreference to the quote style (' or ") captured
var myRegexp = /([^\s'"]([^\s'"]*(['"])([^\3]*?)\3)+[^\s'"]*)|[^\s'"]+|(['"])([^\5]*?)\5/gi;
var myString = value;
var myArray = [];
if (env) {
myArray.push(env);
}
if (file) {
myArray.push(file);
}
var match;
do {
// Each call to exec returns the next regex match as an array
match = myRegexp.exec(myString);
if (match !== null) {
// Index 1 in the array is the captured group if it exists
// Index 0 is the matched text, which we use if no captured group exists
myArray.push(this.firstString(match[1], match[6], match[0]));
}
} while (match !== null);
return myArray;
}
// Exact copy of https://github.com/mccormicka/string-argv
// Accepts any number of arguments, and returns the first one that is a string
// (even an empty string)
firstString() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (typeof arg === "string") {
return arg;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment