bauhausjs
Version:
A modular CMS for Node.js
232 lines (192 loc) • 10.5 kB
JavaScript
var gm = require ('gm').subClass({ imageMagick: true }),
express = require('express'),
Asset = require('./model/asset');
/**
* Returns express middleware which retruns assets with stored mime type
*
*/
module.exports = function (bauhausConfig) {
'use strict';
var app = express();
app.param('id', function (req, res, next, id) {;
if (typeof id === 'string' && id.length === 24) {
next();
} else {
next(new Error('Invalid Asset id'));
}
});
/*
* Route to view the assets
*
*/
app.get('/:id', function (req, res, next) {
var routeParams = req.route.params,
id = routeParams.id,
query = req.query; //contains the urls query parameters
Asset.findOne ({_id : id},'name metadata data',function (err,assetDocument) {
var options;
if (err) { return next(err.message); } // Mongo error
if (!assetDocument) { return next(); } // No asset found
if (assetDocument && !assetDocument.data) { return next('No content'); } // Asset has no content
if (assetDocument && assetDocument.metadata && !assetDocument.metadata['content-type']) { return next('Missing Metadata'); } // Asset has no metadata
// temporary fix to avoid errors on non-images
var isImage = /^image\//;
if (assetDocument.metadata['content-type'].match(isImage) === null) { return next('Not an image'); } // Asset has no metadata
options = {};
if (typeof query.transform !== 'undefined') {
options.transform = {
width : query.width,
height: query.height,
cropCoords: query.cropCoords, //cropping coords left - top : right - bottom e.g: [[20,20],[30,30]] cuts 20px from the left and top, and 30 px from right and bottom
scale_mode: query.scale_mode?query.scale_mode:'fit', //gm options for resizing, "!" forces a resize, ignoring the original aspect ratio
transform: query.transform //optional transformation order ['resize','crop']
};
}
sendAsset (res, assetDocument, options);
});
});
/*
* Sends the buffer of the asset to the client.
* Accepts options to perform transformations on the asset.
*/
function sendAsset (res,assetDocument,options) {
var data,
id;
if (assetDocument.metadata && assetDocument.metadata['content-type']) {
res.setHeader('Content-Type', assetDocument.metadata['content-type']);
} else {
res.setHeader('Content-Type', 'text/plain');
}
data = assetDocument.data;
id = assetDocument.id;
/*
* Performs transformations on an image
* TODO: Think about a more generic way to perform other transformations.
* TODO: Implement filters
*/
if (options && options.transform) {
var aspectRatio = assetDocument.metadata.aspectRatio.value,
width = parseInt (options.transform.width,10) || assetDocument.metadata.width, //Use the original values, since the transformation uses the lower value, aspect ratio gets corrected
height = parseInt (options.transform.height,10) || assetDocument.metadata.height,
scale_mode = options.transform.scale_mode==='fit'?'':'!', //gm accepts an third parameter "options" which forces a resize when set to '!'
transformData = {}, // contains the data, applied to their respectice transform function from gm
transformOrderDef = ["resize","crop"], //The default transformorder
transformOrder = options.transform.transform, //Weakly match length to filter all falsy values, if no transform order was specified, use the default one.
cropCoords = options.transform.cropCoords,
cleanedCoords = [], // [width,height,x,y]
cropSize,
parsedCoords, //JSON parsed cropCoords
gmInstance,
tmpCond, //used to temporarily assign test conditions
query,
max;
//Parse the transformorder
if (transformOrder) {
try {
transformOrder = JSON.parse (transformOrder); //Should be assigned to its own variable
} catch (e) {
transformOrder = transformOrderDef;
}
} else {
transformOrder = transformOrderDef;
}
/*
* preserve the right values for width and height, to match the aspect ratio if scale mode is fit
* We need to normalize the data to e.g. get the same cached version for w: 50 h: 7889 and w:50 h: 123
*/
if (scale_mode !== '!') { //'' == false.
max = Math.max (width,height);
if (width === max) {
width = Math.round (height * aspectRatio); //Round it if the aspect ratio * height is not an int
} else if (height === max) {
height = Math.round (width / aspectRatio);
}
}
//If the aspect ratio matches the original, scale_mode:fill should be corrected, to make sure the cached assets are the same
if ((width / height).toPrecision (5) === aspectRatio.toPrecision (5)) {
scale_mode = '';
}
// reformat the cropcoords vlaue to be able to directly apply it on the crop function of gm
if (typeof cropCoords !== 'undefined') {
try {
parsedCoords = JSON.parse (cropCoords);
//The sum of the cropcoords, only a value > 0 makes sense...
tmpCond = parsedCoords.reduce (function sum (a,b) {
return a + (Array.isArray (b)?b.reduce (sum,0):b);
},0);
if (transformOrder.indexOf ("crop") > transformOrder.indexOf ("resize")) {
cropSize = [width,height]; //If the cropping should happen after the resizing, we should use the resized images width and height for reference
} else {
cropSize = [assetDocument.metadata.width,assetDocument.metadata.height]; //otherwise we use the original image size as reference for the cropping
}
//TODO: Handle negative new width and rheight values
if (tmpCond > 0) {
cleanedCoords [0] = cropSize [0] - parsedCoords [1][0]; //new width
cleanedCoords [1] = cropSize [1] - parsedCoords [1][1]; //new height
cleanedCoords [2] = parsedCoords [0][0]; //x
cleanedCoords [3] = parsedCoords [0][1]; //y
}
tmpCond = null;
} catch (e) {
//don't crop the image if false values have been passed
}
}
//If the recaalculated width and height either match the original size or are larger, use the original image.
if (width >= assetDocument.metadata.width && height >= assetDocument.metadata.height && cleanedCoords.length === 0) {
res.writeHead(200);
return res.end(data);
}
query = {
parentId: id,
'transforms.width':width,
'transforms.height':height,
'transforms.scale_mode':scale_mode,
'transforms.cropCoords': cleanedCoords,
'transforms.transformOrder': transformOrder
}; //The query to find the cached version, if existent
Asset.findOne (query,'parentId transforms data', function (err, cachedAssetDocument) {
if (!cachedAssetDocument) { //If no transformed asset exists with the given parameters
cachedAssetDocument = new Asset (); //we create a new one, and save it in the mongodb
//Define the data that should be passed to the different transformation methods
transformData.resize = [width, height, scale_mode];
transformData.crop = cleanedCoords;
gmInstance = gm (data);
/* Iterate over the transformations that shopuld be performed, in their (optionally) defined order.
* check if we have a defined dataset that can be applied to the transformation
* and if the data exists
*/
for (var i = 0,transformation,currentData; i < transformOrder.length; i++) {
transformation = transformOrder [i];
currentData = transformData [transformation];
if (currentData && currentData.length > 0) {
gmInstance [transformation].apply (gmInstance, currentData);
}
}
//When the transformations were applied, save the resulting image and return its buffer
gmInstance.toBuffer (function (err,buffer) {
if (err) { next(err); }
cachedAssetDocument.data = buffer;
cachedAssetDocument.parentId = id;
cachedAssetDocument.transforms = {
width: width,
height: height,
cropCoords: cleanedCoords,
transformOrder: transformOrder,
scale_mode: scale_mode
};
cachedAssetDocument.save ( function (err) {
if (err) { next(err); }
res.send (buffer);
});
});
} else { //Otherwise send back the cached version
res.send (cachedAssetDocument.data);
}
});
} else {
res.writeHead(200);
return res.end(data);
}
}
return app;
};