Last active
April 18, 2022 09:29
-
-
Save farukcankaya/5b698044d907fe80a7c1f6e3afe618bc to your computer and use it in GitHub Desktop.
Using custom/complex imagemagick command with GraphicsMagick for node
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ImageConverterOperator { | |
constructor (operator, parameters) { | |
this.operator = operator | |
this.parameters = parameters | |
this.name = this.constructor.name | |
} | |
} | |
module.exports = ImageConverterOperator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
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