Skip to content

Commit

Permalink
use pure javascript image manipulation in the development backend ins…
Browse files Browse the repository at this point in the history
…tead of ImageMagick

A lot slower but doesn't need native dependencies, and we just need it for development.
  • Loading branch information
bago committed May 25, 2022
1 parent 6d0bb60 commit aaf61fa
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 56 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM node:8-alpine

RUN apk update
RUN apk add bzip2 tar git imagemagick
RUN apk add bzip2 tar git

RUN npm install grunt-cli -g

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.CentOS
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM centos:centos8

RUN yum clean all
RUN yum -y install epel-release; yum clean all
RUN yum -y install bzip2 tar git nodejs npm GraphicsMagick; yum clean all
RUN yum -y install bzip2 tar git nodejs npm; yum clean all

RUN npm install grunt-cli -g

Expand Down
4 changes: 2 additions & 2 deletions backend/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Provides the following services:

/dl
receives a post with the html body and a parameter asking for "download" or "email".
(it does inlining using Styliner) since Mosaico 0.15 CSS inlining happens in the client.
(it does inlining using Juice) since Mosaico 0.15 CSS inlining happens in the client.
if asked to send an email it sends it using nodemailer

/upload
Expand All @@ -18,6 +18,6 @@ Provides the following services:
"placeholder" will return a placeholder image with the given width/height (encoded in params as "width,height")
"cover" will resize the image keeping the aspect ratio and covering the whole dimension (cutting it if different A/R)
"resize" can receive one dimension to resize while keeping the A/R, or 2 to resize the image to be inside the dimensions.
this uses "gm" library to do manipulation (you need ImageMagick installed in your system).
this uses "jimp" library to do manipulation (not very fast/secure, but we only use it for development).

This currently doesn't provide any authentication or security options, so don't use this in production!
140 changes: 89 additions & 51 deletions backend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ var bodyParser = require('body-parser');
var fs = require('fs');
var _ = require('lodash');
var app = express();
var gm = require('gm').subClass({imageMagick: true});
var config = require('../server-config.js');
var extend = require('util')._extend;
var url = require('url');
var mime = require('mime-types');
var Jimp = require('jimp');
var font;
Jimp.loadFont(Jimp.FONT_SANS_32_BLACK, function(err, f) {
font = f;
});

app.use(require('connect-livereload')({ ignore: [/^\/dl/, /^\/img/, /^\/upload/] }));

Expand Down Expand Up @@ -95,66 +99,104 @@ app.get('/img/', function(req, res) {
var params = req.query.params.split(',');

if (req.query.method == 'placeholder') {
var out = gm(params[0], params[1], '#808080');
res.set('Content-Type', 'image/png');
var x = 0, y = 0;
var size = 40;
// stripes
while (y < params[1]) {
out = out
.fill('#707070')
.drawPolygon([x, y], [x + size, y], [x + size*2, y + size], [x + size*2, y + size*2])
.drawPolygon([x, y + size], [x + size, y + size*2], [x, y + size*2]);
x = x + size*2;
if (x > params[0]) { x = 0; y = y + size*2; }
}
// text
out.fill('#B0B0B0').fontSize(20).drawText(0, 0, params[0] + ' x ' + params[1], 'center').stream('png').pipe(res);
var w = parseInt(params[0]);
var h = parseInt(params[1]);
var workScale = 1;

new Jimp(w * workScale, h * workScale, '#808080', function(err, image) {
image.scan(0, 0, image.bitmap.width, image.bitmap.height, function(x, y, idx) {
if ((((Math.ceil(image.bitmap.height / (size * workScale * 2))+1)*(size * workScale * 2) + x - y) % (size * workScale * 2)) < size * workScale) image.setPixelColor(0x707070FF, x, y);
if (x == image.bitmap.width - 1 && y == image.bitmap.height - 1) {
var tempImg = new Jimp(w * workScale, h * workScale, 0x0)
.print(font, 0, 0, {
text: '' + w + ' x ' + h,
alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER,
alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE
}, w * workScale, h * workScale)
.color([{ apply: 'xor', params: ['#B0B0B0'] }], function (err, tempImg2) {
if (err) {
console.log("Error #1 creating placeholder: ", err);
res.status(500);
} else {
image.blit(tempImg2, 0, 0)
.getBuffer(Jimp.MIME_PNG, function(error, buffer) {
if (error) {
console.log("Error #2 creating placeholder: ", error);
res.status(500);
} else res.status(200).send(new Buffer(buffer));
});
}
});
}
});
});

} else if (req.query.method == 'resize' || req.query.method == 'cover' || req.query.method == 'aspect') {
// NOTE: req.query.src is an URL but gm is ok with URLS.
// We do parse it to localpath to avoid strict "securityPolicy" found in some ImageMagick install to prevent the manipulation
// NOTE: req.query.src is an URL: we do parse it to localpath.
var urlparsed = url.parse(req.query.src);
var src = "./"+decodeURI(urlparsed.pathname);

var ir = gm(src);
var ir = Jimp.read(src, function(err, image) {

var format = function(err,format) {
if (!err) {
res.set('Content-Type', 'image/'+format.toLowerCase());
if (req.query.method == 'resize') {
ir.autoOrient().resize(params[0] == 'null' ? null : params[0], params[1] == 'null' ? null : params[1]).stream().pipe(res);
} else {
ir.autoOrient().resize(params[0],params[1]+'^').gravity('Center').extent(params[0], params[1]+'>').stream().pipe(res);
}
if (err) {
console.log("Error reading image: ", err);
res.status(404);
} else {
console.error("ImageMagick failed to detect image format for", src, ". Error:", err);
res.status(404).send('Error: '+err);
}
};

// "aspect" method is currently unused, but we're evaluating it.
if (req.query.method == 'aspect') {
ir.size(function(err, size) {
if (!err) {
// "aspect" method is currently unused, but we're evaluating it.
if (req.query.method == 'aspect') {
var oldparams = [ params[0], params[1] ];
if (params[0] / params[1] > size.width / size.height) {
params[1] = Math.round(size.width / (params[0] / params[1]));
params[0] = size.width;
if (params[0] / params[1] > image.bitmap.width / image.bitmap.height) {
params[1] = Math.round(image.bitmap.width / (params[0] / params[1]));
params[0] = image.bitmap.width;
} else {
params[0] = Math.round(image.bitmap.height * (params[0] / params[1]));
params[1] = image.bitmap.height;
}
}

// res.set('Content-Type', 'image/'+format.toLowerCase());
var sendOrError = function(err, image) {
if (err) {
console.log("Error manipulating image: ", err);
res.status(500);
} else {
params[0] = Math.round(size.height * (params[0] / params[1]));
params[1] = size.height;
image.getBuffer(Jimp.MIME_PNG, function(error, buffer) {
if (error) {
console.log("Error sending manipulated image: ", error);
res.status(500);
} else res.status(200).send(new Buffer(buffer));
});
}
// console.log("Image size: ", size, oldparams, params);
ir.format(format);
};
if (req.query.method == 'resize') {
if (params[0] == 'null')
image.resize(Jimp.AUTO, parseInt(params[1]), sendOrError);
else if (params[1] == 'null')
image.resize(parseInt(params[0]), Jimp.AUTO, sendOrError);
else
image.contain(parseInt(params[0]), parseInt(params[1]), sendOrError);
} else {
console.error("ImageMagick failed to detect image size for", src, ". Error:", err);
res.status(404).send('Error: '+err);
// Compute crop coordinates for cover algorythm
var w = parseInt(params[0]);
var h = parseInt(params[1]);
var ar = w/h;
var origAr = image.bitmap.width/image.bitmap.height;
if (ar > origAr) {
var newH = Math.round(image.bitmap.width / ar);
var newY = Math.round((image.bitmap.height - newH) / 2);
image.crop(0, newY, image.bitmap.width, newH).resize(w, h, sendOrError);
} else {
var newW = Math.round(image.bitmap.height * ar);
var newX = Math.round((image.bitmap.width - newW) / 2);
image.crop(newX, 0, newW, image.bitmap.height).resize(w, h, sendOrError);
}
}
});
} else {
ir.format(format);
}

}

});
}

});
Expand Down Expand Up @@ -201,10 +243,6 @@ app.use('/uploads', express.static(__dirname + '/../uploads'));
app.use(express.static(__dirname + '/../dist/'));

var server = app.listen( PORT, function() {
var check = gm(100, 100, '#000000');
check.format(function (err, format) {
if (err) console.error("ImageMagick failed to run self-check image format detection. Error:", err);
});
console.log('Express server listening on port ' + PORT);
} );

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"cheerio": "1.0.0-rc.5",
"connect-livereload": "0.6.1",
"express": "4.18.0",
"gm": "1.23.1",
"grunt": "^1.5.2",
"grunt-browserify": "6.0.0",
"grunt-contrib-clean": "2.0.1",
Expand All @@ -93,6 +92,7 @@
"grunt-parallel": "0.5.1",
"grunt-release": "0.13.1",
"jasmine-core": "4.1.0",
"jimp": "^0.16.1",
"jshint-stylish": "2.2.1",
"license-checker": "25.0.1",
"load-grunt-tasks": "5.1.0",
Expand Down

0 comments on commit aaf61fa

Please sign in to comment.