UNPKG

11.9 kBJavaScriptView Raw
1/**
2 * Module dependencies
3 */
4
5var envs = require('envs');
6var Transloadit = require('transloadit');
7var proxy = require('simple-http-proxy');
8
9function noop() {}
10
11/**
12 * Initialize default params
13 */
14
15var params = [
16 ['width', null, '1-5000', 'Width of the new image, in pixels'],
17 ['height', null, '1-5000', 'Height of the new image, in pixels'],
18 ['strip', false, 'boolean', 'Strips all metadata from the image. This is useful to keep thumbnails as small as possible.'],
19 ['flatten', true, 'boolean', 'Flattens all layers onto the specified background to achieve better results from transparent formats to non-transparent formats, as explained in the ImageMagick documentation.\nNote To preserve animations, GIF files are not flattened when this is set to true. To flatten GIF animations, use the frame parameter.'],
20 ['correct_gamma', false, 'boolean', 'Prevents gamma errors common in many image scaling algorithms.'],
21 ['quality', 92, '1-100', 'Controls the image compression for JPG and PNG images.'],
22 ['background', '#FFFFFF', 'string', 'Either the hexadecimal code or name of the color used to fill the background (only used for the pad resize strategy).'],
23 ['resize_strategy', 'fit', 'fit|stretch|pad|crop|fillcrop', 'https://transloadit.com/docs/conversion-robots#resize-strategies'],
24 ['zoom', true, 'boolean', 'If this is set to false, smaller images will not be stretched to the desired width and height. For details about the impact of zooming for your preferred resize strategy, see the list of available resize strategies.'],
25 ['format', null, 'jpg|png|gif|tiff', 'The available formats are "jpg", "png", "gif", and "tiff".'],
26 ['gravity', 'center', 'center|top|bottom|left|right', 'The direction from which the image is to be cropped. The available options are "center", "top", "bottom", "left", and "right". You can also combine options with a hyphen, such as "bottom-right".'],
27 ['frame', null, 'integer', 'Use this parameter when dealing with animated GIF files to specify which frame of the GIF is used for the operation. Specify 1 to use the first frame, 2 to use the second, and so on.'],
28 ['colorspace', null, 'string', 'Sets the image colorspace. For details about the available values, see the ImageMagick documentation. Please note that if you were using "RGB", we recommend using "sRGB" instead as of 2014-02-04. ImageMagick might try to find the most efficient colorspace based on the color of an image, and default to e.g. "Gray". To force colors, you might then have to use this parameter in combination with type "TrueColor"'],
29 ['type', null, 'string', 'Sets the image color type. For details about the available values, see the ImageMagick documentation. If you\'re using colorspace, ImageMagick might try to find the most efficient based on the color of an image, and default to e.g. "Gray". To force colors, could e.g. set this parameter to "TrueColor"'],
30 ['sepia', null, 'number', 'Sets the sepia tone in percent. Valid values range from 0 - 99.'],
31 ['rotation', true, 'string|boolean|integer', 'Determines whether the image should be rotated. Set this to true to auto-rotate images that are rotated in a wrong way, or depend on EXIF rotation settings. You can also set this to an integer to specify the rotation in degrees. You can also specify "degrees" to rotate only when the image width exceeds the height (or "degrees" if the width must be less than the height). Specify false to disable auto-fixing of images that are rotated in a wrong way.'],
32 ['compress', null, 'string', 'Specifies pixel compression for when the image is written. Valid values are None, "BZip", "Fax", "Group4", "JPEG", "JPEG2000", "Lossless", "LZW", "RLE", and "Zip". Compression is disabled by default.'],
33 ['blur', null, 'string', 'Specifies gaussian blur, using a value with the form {radius}x{sigma}. The radius value specifies the size of area the operator should look at when spreading pixels, and should typically be either "0" or at least two times the sigma value. The sigma value is an approximation of how many pixels the image is "spread"; think of it as the size of the brush used to blur the image. This number is a floating point value, enabling small values like "0.5" to be used.'],
34
35 ['crop_x1'],
36 ['crop_y1'],
37 ['crop_x2'],
38 ['crop_y2'],
39
40 ['text'],
41 ['progressive', true, 'boolean', 'Interlaces the image if set to true, which makes the image load progressively in browsers. Instead of rendering the image from top to bottom, the browser will first show a low-res blurry version of the images which is then quickly replaced with the actual image as the data arrives. This greatly increases the user experience, but comes at a cost of a file size increase by around 10%.'],
42 ['transparent', null, 'string', 'Make this color transparent within the image.'],
43 ['clip', false, 'mixed', 'Apply the clipping path to other operations in the resize job, if one is present. If set to true, it will automatically take the first clipping path. If set to a string it finds a clipping path by that name.'],
44 ['negate', false, 'boolean', 'Replace each pixel with its complementary color, effictively negating the image. Especially useful when testing clipping.'],
45 ['density', null, 'string', 'While in-memory quality and file format depth specifies the color resolution, the density of an image is the spatial (space) resolution of the image. That is the density (in pixels per inch) of an image and defines how far apart (or how big) the individual pixels are. It defines the size of the image in real world terms when displayed on devices or printed.\n\nYou can set this value to a specific width or in the format widthxheight.\n\nIf your converted image has a low resolution, please try using the density parameter to resolve that.'],
46 ['force_accept', false, 'boolean', 'Robots may accept only certain file types - all other possible input files are ignored. \nThis means the /video/encode robot for example will never touch an image while the /image/resize robot will never look at a video.\nWith the force_accept parameter you can force a robot to accept all files thrown at him, regardless if it would normally accept them.'],
47 ['watermark_url', null, 'string', 'A url indicating a PNG image to be overlaid above this image. Please note that you can also supply the watermark via another assembly step.'],
48 ['watermark_position', 'center', 'string', 'The position at which the watermark is placed. The available options are "center", "top", "bottom", "left", and "right". You can also combine options, such as "bottom-right".\n\nThis setting puts the watermark in the specified corner. To use a specific pixel offset for the watermark, you will need to add the padding to the image itself.'],
49 ['watermark_size', null, 'string', 'The size of the watermark, as a percentage.\nFor example, a value of "50%" means that size of the watermark will be 50% of the size of image on which it is placed.'],
50 ['watermark_resize_strategy', 'fit', 'string', 'Available values are "fit" and "stretch".']
51];
52
53/**
54 * Initialize the imago middleware
55 *
56 * @param {Object} opts
57 * @return {Function}
58 */
59
60module.exports = function(opts) {
61 opts = opts || {};
62
63 var client = createClient(opts);
64
65 var bucket = opts.s3Bucket || envs('S3_BUCKET');
66 var key = opts.s3Key || envs('AWS_ACCESS_KEY_ID');
67 var secret = opts.s3Secret || envs('AWS_SECRET_ACCESS_KEY');
68 var transformAssembly = opts.onassembly || noop;
69
70 return function processImage(req, res, next) {
71 if (req.url === '/') return api(req, res, next);
72
73 var assembly = {
74 params: {
75 steps: steps(key, secret, bucket, req)
76 }
77 };
78
79 set(req.query, assembly.params, 'template_id');
80
81 var transformed = (transformAssembly || noop)(assembly, req) || assembly;
82
83 var end = profile(req, 'transloadit.resize');
84 client(transformed, function(err, result) {
85 end({
86 error: err && err.message,
87 assembly: result && result.assembly_url
88 });
89
90 if (err) return next(err);
91 var url = result.results.out[0].url;
92 req.url = '';
93
94 res.on('header', function() {
95 if (res.statusCode !== 200) return;
96 res.set('cache-control', 'max-age=31536000, public');
97 });
98
99 proxy(url)(req, res, next);
100 });
101 };
102};
103
104function api(req, res, next) {
105 res.json({
106 params: params.reduce(function(acc, args) {
107 var key = args[0];
108 acc[key] = {
109 type: args[2],
110 value: args[1],
111 info: args[3]
112 };
113 return acc;
114 }, {})
115 });
116}
117
118/**
119 * Create a transloadit client
120 *
121 * @param {Object} opts
122 * @return {Function}
123 */
124
125function createClient(opts) {
126 var client = new Transloadit({
127 authKey : opts.transloaditAuthKey || envs('TRANSLOADIT_AUTH_KEY'),
128 authSecret : opts.transloaditAuthSecret || envs('TRANSLOADIT_SECRET_KEY')
129 });
130
131 function create(assembly, cb) {
132 client.createAssembly(assembly, handle.bind(null, cb, null));
133 }
134
135 function poll(url, cb) {
136 setTimeout(function() {
137 client._remoteJson({
138 url: url
139 }, handle.bind(null, cb, url));
140 }, 500);
141 }
142
143 function handle(cb, url, err, result) {
144 if (err && (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT') && url) return poll(url, cb);
145 if (err) return cb(err);
146 if (result.error) return cb(new Error(result.message));
147 if (result.ok !== 'ASSEMBLY_COMPLETED') return poll(result.assembly_url, cb);
148 return cb(err, result);
149 }
150
151 return create;
152}
153
154/**
155 * Profile a transloadit request
156 *
157 * @param {Request} req
158 * @param {String} str
159 * @param {Object} opts
160 * @return {Function}
161 */
162
163function profile(req, str, opts) {
164 var metric = req.metric;
165 if (!metric) return noop;
166 var profile = metric.profile;
167 if (!profile) return noop;
168 return req.metric.profile(str, opts);
169}
170
171/**
172 * Format the out params
173 *
174 * @param {String} use
175 * @param {Object} query
176 * @return {Object}
177 */
178
179function formatOut(use, query) {
180 var assembly = {
181 robot: '/image/resize',
182 use: use
183 };
184
185 var s = set.bind(null, query, assembly);
186
187 params.forEach(function(args) {
188 s.apply(null, args);
189 });
190
191 if (assembly.text) deepParse(assembly.text);
192
193 return assembly;
194}
195
196/**
197 * Set a value on the assembly
198 *
199 * @param {Object} query
200 * @param {Object} assembly
201 * @param {String} key
202 * @param {Any} defaultValue
203 * @param {Function?} transform
204 */
205
206function set(query, assembly, key, defaultValue) {
207 var val = query[key];
208
209 if (typeof val !== 'undefined') assembly[key] = val;
210 else if (defaultValue !== null) assembly[key] = defaultValue;
211
212 if (assembly[key] !== null) assembly[key] = deepParse(assembly[key]);
213}
214
215/**
216 * Calculate assembly steps
217 *
218 * @param {String} key
219 * @param {String} secret
220 * @param {String} bucket
221 * @param {Object} req
222 */
223function steps(key, secret, bucket, req) {
224 var outPrevStep = 'import';
225 var steps = {
226 import: {
227 robot: '/s3/import',
228 key: key,
229 secret: secret,
230 bucket: bucket,
231 path: req.url.split('?')[0].substr(1)
232 }
233 }
234 //cropping passed
235 if (req.query.crop) {
236 steps['crop'] = {
237 robot: '/image/resize',
238 use: 'import',
239 crop: deepParse(req.query.crop),
240 resize_strategy: "crop"
241 };
242
243 outPrevStep = 'crop';
244 }
245
246 steps['out'] = formatOut(outPrevStep, req.query);
247 return steps;
248}
249
250/**
251 * Deep parse an object
252 */
253
254function deepParse(val) {
255 if (typeof val !== 'object') return parse(val);
256 if (Array.isArray(val)) return val.map(deepParse);
257 return Object.keys(val).reduce(function(acc, key) {
258 acc[key] = deepParse(val[key])
259 return acc;
260 }, {});
261}
262
263
264/**
265 * Parse a query value
266 *
267 * @param {String} val
268 * @return {String|Boolean|Number}
269 */
270
271function parse(val) {
272 if (val === 'true') return true;
273 if (val === 'false') return false;
274 var num = parseInt(val);
275 if (!isNaN(num)) return num;
276 return val;
277}