UNPKG

11.3 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: {
76 import: {
77 robot: '/s3/import',
78 key: key,
79 secret: secret,
80 bucket: bucket,
81 path: req.url.split('?')[0].substr(1)
82 },
83 out: formatOut('import', req.query)
84 }
85 }
86 };
87
88 set(req.query, assembly.params, 'template_id');
89
90 var transformed = (transformAssembly || noop)(assembly, req);
91
92 var end = profile(req, 'transloadit.resize');
93 client(transformed || assembly, function(err, result) {
94 end({
95 error: err && err.message,
96 assembly: result && result.assembly_url
97 });
98
99 if (err) return next(err);
100 var url = result.results.out[0].url;
101 req.url = '';
102
103 res.on('header', function() {
104 if (res.statusCode !== 200) return;
105 res.set('cache-control', 'max-age=31536000, public');
106 });
107
108 proxy(url)(req, res, next);
109 });
110 };
111};
112
113function api(req, res, next) {
114 res.json({
115 params: params.reduce(function(acc, args) {
116 var key = args[0];
117 acc[key] = {
118 type: args[2],
119 value: args[1],
120 info: args[3]
121 };
122 return acc;
123 }, {})
124 });
125}
126
127/**
128 * Create a transloadit client
129 *
130 * @param {Object} opts
131 * @return {Function}
132 */
133
134function createClient(opts) {
135 var client = new Transloadit({
136 authKey : opts.transloaditAuthKey || envs('TRANSLOADIT_AUTH_KEY'),
137 authSecret : opts.transloaditAuthSecret || envs('TRANSLOADIT_SECRET_KEY')
138 });
139
140 function create(assembly, cb) {
141 client.createAssembly(assembly, handle.bind(null, cb));
142 }
143
144 function poll(url, cb) {
145 setTimeout(function() {
146 client._remoteJson({
147 url: url
148 }, handle.bind(null, cb));
149 }, 500);
150 }
151
152 function handle(cb, err, result) {
153 if (err) return cb(err);
154 if (result.error) return cb(new Error(result.message));
155 if (result.ok !== 'ASSEMBLY_COMPLETED') return poll(result.assembly_url, cb);
156 cb(err, result);
157 }
158
159 return create;
160}
161
162/**
163 * Profile a transloadit request
164 *
165 * @param {Request} req
166 * @param {String} str
167 * @param {Object} opts
168 * @return {Function}
169 */
170
171function profile(req, str, opts) {
172 var metric = req.metric;
173 if (!metric) return noop;
174 var profile = metric.profile;
175 if (!profile) return noop;
176 return req.metric.profile(str, opts);
177}
178
179/**
180 * Format the out params
181 *
182 * @param {String} use
183 * @param {Object} query
184 * @return {Object}
185 */
186
187function formatOut(use, query) {
188 var assembly = {
189 robot: '/image/resize',
190 use: use
191 };
192
193 var s = set.bind(null, query, assembly);
194
195 params.forEach(function(args) {
196 s.apply(null, args);
197 });
198
199 return assembly;
200}
201
202/**
203 * Set a value on the assembly
204 *
205 * @param {Object} query
206 * @param {Object} assembly
207 * @param {String} key
208 * @param {Any} defaultValue
209 * @param {Function?} transform
210 */
211
212function set(query, assembly, key, defaultValue) {
213 var val = query[key];
214 if (typeof val !== 'undefined') assembly[key] = val;
215 else if (defaultValue !== null) assembly[key] = defaultValue;
216
217 if (assembly[key] !== null) assembly[key] = deepParse(assembly[key]);
218}
219
220/**
221 * Deep parse an object
222 */
223
224function deepParse(val) {
225 if (typeof val !== 'object') return parse(val);
226 if (Array.isArray(val)) return val.map(deepParse);
227 return Object.keys(val).reduce(function(acc, key) {
228 acc[key] = deepParse(val[key])
229 return acc;
230 }, {});
231}
232
233/**
234 * Parse a query value
235 *
236 * @param {String} val
237 * @return {String|Boolean|Number}
238 */
239
240function parse(val) {
241 if (val === 'true') return true;
242 if (val === 'false') return false;
243 var num = parseInt(val);
244 if (!isNaN(num)) return num;
245 return val;
246}