Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ Getting up and running is as easy as 1, 2, 3.
GET /local/get/:key

GET /local/get/:key?w_:width,h_:height

GET /local/get/:key?w_:width,h_:height,hc_:crop_height,wc_:crop_width,c_:crop_direction
GET /local/get/:key?hc_:crop_height,wc_:crop_width,c_:crop_direction



## Deployment
Microservice is deployable as Docker image, following named volumes are defined:
Expand All @@ -71,15 +76,25 @@ Microservice is deployable as Docker image, following named volumes are defined:
Microservice uses JWT-based token authentication (https://jwt.io). Token must be configured using JWT_SECRET environment variable. Tokens should be passed with 'Authorization' request header, using the Bearer schema (https://tools.ietf.org/html/rfc6750)

## Local image storage and getting resized images
Images uploaded to local storage can be resized on get, using GET parameters passed to route in following format
Images uploaded to local storage can be resized or(and) cropped on get, using GET parameters passed to route in following format

w_:width - :width - positive number, target width in pixels, if given number is lesser than 1 - ratio to original width

h_:height - :height - positive number, target height in pixels, if given number is lesser than 1 - ratio to original height


hc_:crop_height - :crop_height - positive number, target width in pixels, if given number is lesser than 1 - ratio to original width

wc_:crop_width - :crop_width - positive number, target width in pixels, if given number is lesser than 1 - ratio to original width

c_:crop_direction - :crop_direction - one of the given string: NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast

In case if only width or height given - target image is resized to that dimension keeping aspect ratio.

In case if both width and height are given - target image is resized to given dimensions without keeping the aspect ratio.

In case if crop_width, crop_height and crop_direction given - target image is croped to given dimensions.

You can also mix height, width, crop_height, crop_width and crop_direction params for cropping and resizing at once.

## Testing

Expand Down
64 changes: 64 additions & 0 deletions src/helpers/crop/crop_image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const paramsHash = require('./crop_params_hash');
const paramsSanitize = require('./crop_params_sanitize');
const path = require('path');
const db = require('./../db')();
const config = require('smart-config').get('local');
const gm = require('gm').subClass({
imageMagick: true
});

module.exports = (filename, fullname, params) => {

return new Promise((resolve, reject) => {
let results = {
fileName: filename,
filePath: fullname
};

if (Object.keys(params).length === 0) {
return resolve(results);
}

const strHash = paramsHash(params, filename);

db.getCached(strHash, params)
.then((cached) => {
results.filePath = path.join(config.files_path, "/", cached);
return resolve(results);
},
(err) => {
if (err !== false) {
return reject(err);
}

let sourceImage = gm(fullname);

sourceImage.size((err, size) => {
if (err) {
return reject(err);
}
let resizeDimensions = paramsSanitize
.parseParams(params.width || 0, params.height || 0, size.width, size.height);

if (resizeDimensions.width > 0 && resizeDimensions.height > 0) {
sourceImage.gravity(params.crop).crop(resizeDimensions.width, resizeDimensions.height);
}

const resultFilename = path.join(strHash + results.fileName);
sourceImage.write(path.join(config.files_path, "/", resultFilename), (err) => {
if (err) {
reject(err);
}
db.saveCached(strHash, resultFilename).then(() => {
results.filePath = path.join(config.files_path, "/", resultFilename);
resolve(results);
}).catch((err) => {
reject(err);
});
});
});

});

});
};
11 changes: 11 additions & 0 deletions src/helpers/crop/crop_params_hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const crypto = require('crypto');

module.exports = (params, filename) => {
let strParamsValue = filename;
Object.keys(params).forEach((index) => {
strParamsValue += '' + index + params[index];
});
const hash = crypto.createHash('sha256');
hash.update(strParamsValue);
return hash.digest('hex');
};
28 changes: 28 additions & 0 deletions src/helpers/crop/crop_params_parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

module.exports = (query) => {
let results = [];

Object.keys(query).forEach((queryItem) => {
let paramsList = [queryItem];
if (String(queryItem).indexOf(',') !== -1) {
paramsList = queryItem.split(',');
}

paramsList.forEach((paramItem) => {
let widthParsed = /wc_(.*)/.exec(paramItem);
if (widthParsed) {
results.width = widthParsed[1];
}
let heightParsed = /hc_(.*)/.exec(paramItem);
if (heightParsed) {
results.height = heightParsed[1];
}
let cropParsed = /c_(.*)/.exec(paramItem);
if (cropParsed) {
results.crop = cropParsed[1];
}
});
});

return results;
};
33 changes: 33 additions & 0 deletions src/helpers/crop/crop_params_sanitize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module.exports = {

parseDimension(paramValue, imageDimension) {
if (paramValue <= 0) {
return 0;
}
if (paramValue % 1 > 0) {
if (paramValue < 1) {
paramValue = Math.round(imageDimension * paramValue);
} else {
paramValue = Math.round(paramValue);
}
}
return paramValue;
},

parseParams(width, height, imageWidth, imageHeight) {
width = this.parseDimension(width, imageWidth);
height = this.parseDimension(height, imageHeight);

if (width && height === 0) {
height = Math.round(imageHeight * (width / imageWidth));
}
if (height && width === 0) {
width = Math.round(imageWidth * (height / imageHeight));
}
return {
width,
height
};
}

};
20 changes: 20 additions & 0 deletions src/helpers/crop/crop_params_validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const validator = require('validator');

module.exports = (params) => {
Object.keys(params).forEach((item) => {
if (validator.isDecimal(params[item], { locale: 'en-US' })) {
params[item] = parseFloat(params[item]);
} else if (validator.isInt(params[item], { min: 1 })) {
params[item] = parseInt(params[item], 10);
} else if (validator.isIn(
params[item],
['NorthWest', 'North', 'NorthEast', 'West', 'Center', 'East', 'SouthWest', 'South', 'SouthEast']
)) {
params[item] = params[item];
} else {
delete (params[item]);
}
});

return params;
};
11 changes: 11 additions & 0 deletions src/helpers/crop/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const cropImage = require('./crop_image.js');
const cropValidateParams = require('./crop_params_validate.js');
const cropParseParams = require('./crop_params_parse.js');
const cropHashParams = require('./crop_params_hash.js');

module.exports = {
cropProcess: cropImage,
cropParamsParse: cropParseParams,
cropParamsValidate: cropValidateParams,
cropParamsHash: cropHashParams
};
38 changes: 31 additions & 7 deletions src/helpers/get/get-from-local.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const httpError = require('http-errors');
const LocalDb = require('../db/')();
const path = require('path');
const resize = require('../resize');
const crop = require('../crop');


module.exports = async (key, params) => {
Expand All @@ -26,13 +27,36 @@ function getFile(key, params) {
let resizeParams = resize.paramsParse(params.query);
resizeParams = resize.paramsValidate(resizeParams);

resize.process(filename, fullname, resizeParams)
.then((data) => {
return resolve(data);
})
.catch((err) => {
return reject(err);
});
let cropParams = crop.cropParamsParse(params.query);
cropParams = crop.cropParamsValidate(cropParams);

if (cropParams.crop) {

crop.cropProcess(filename, fullname, cropParams)
.then((data) => {

resize.process(data.filePath.split('/')[2], data.filePath, resizeParams)
.then((data) => {
return resolve(data);
})
.catch((err) => {
return reject(err);
});
})
.catch((err) => {
return reject(err);
});

} else {
resize.process(filename, fullname, resizeParams)
.then((data) => {
return resolve(data);
})
.catch((err) => {
return reject(err);
});
}

});
}).catch((err) => {
return reject(httpError(err.statusCode, err.message));
Expand Down
26 changes: 26 additions & 0 deletions test/contract/local-download.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,29 @@ test('test download file local + resize width non-valid arguments', async (t) =>
t.is(resp.type, 'image/jpeg');
t.is(resp.headers['content-length'], String(t.context.uploaded_filesize));
});

test('test download file local + crop height and width', async (t) => {
const resp = await superTest.get('/local/get/' + t.context.uploaded.key + '?hc_250,wc_250,c_Center');

t.is(resp.statusCode, 200);
gm(resp.body).size((err, size) => {
if (err) {
t.fail(err);
}
t.is(size.height, 250);
t.is(size.width, 250);
});
});

test('test download file local + crop height and width width non-valid arguments', async (t) => {
const resp = await superTest.get('/local/get/' + t.context.uploaded.key + '?hc_250,wc_250,c_TopLeft');

t.is(resp.statusCode, 200);
gm(resp.body).size((err, size) => {
if (err) {
t.fail(err);
}
t.is(size.height, 250);
t.is(size.width, 250);
});
});