1 |
|
2 |
|
3 |
|
4 |
|
5 | var envs = require('envs');
|
6 | var Transloadit = require('transloadit');
|
7 | var proxy = require('simple-http-proxy');
|
8 |
|
9 | function noop() {}
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | var 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 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | module.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 |
|
104 | function 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 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 | function 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 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | function 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 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | function 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 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 | function 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 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | function 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 |
|
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 |
|
252 |
|
253 |
|
254 | function 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 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 | function 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 | }
|