UNPKG

16 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * Defines functions to manipulate images.
5 *
6 * // Load module "imageProcessor"
7 * var fsApi = require('@openveo/api').imageProcessor;
8 *
9 * @module imageProcessor
10 * @main imageProcessor
11 * @class imageProcessor
12 * @static
13 */
14
15var path = require('path');
16var os = require('os');
17var async = require('async');
18var shortid = require('shortid');
19var gm = require('gm').subClass({
20 imageMagick: true
21});
22var fileSystem = process.requireApi('lib/fileSystem.js');
23
24/**
25 * Generates a thumbnail from the given image.
26 *
27 * Destination directory is automatically created if it does not exist.
28 *
29 * @method generateThumbnail
30 * @param {String} imagePath The image absolute path
31 * @param {String} thumbnailPath The thumbnail path
32 * @param {Number} [width] The expected image width (in px)
33 * @param {Number} [height] The expected image height (in px)
34 * @param {Boolean} [crop] Crop the image if the new ratio differs from original one
35 * @param {Number} [quality] Expected quality from 0 to 100 (default to 90 with 100 the best)
36 * @return {Function} callback Function to call when its done with:
37 * - **Error** An error if something went wrong
38 */
39module.exports.generateThumbnail = function(imagePath, thumbnailPath, width, height, crop, quality, callback) {
40 var image = gm(imagePath);
41
42 async.waterfall([
43
44 // Create thumbnail directory if it does not exist
45 function(callback) {
46 fileSystem.mkdir(path.dirname(thumbnailPath), function(error) {
47 callback(error);
48 });
49 },
50
51 // Get original image size
52 function(callback) {
53 image.size(callback);
54 },
55
56 // Generate thumbnail
57 function(size, callback) {
58 var ratio = size.width / size.height;
59 var cropPosition = {};
60 var resizeWidth = width || Math.round(height * ratio);
61 var resizeHeight = height || Math.round(width / ratio);
62
63 if (crop && width && height) {
64 if (ratio < width / height) {
65 resizeHeight = Math.round(width / ratio);
66 cropPosition = {x: 0, y: Math.round((resizeHeight - height) / 2)};
67 crop = resizeHeight > height;
68 } else {
69 resizeWidth = Math.round(height * ratio);
70 cropPosition = {x: Math.round((resizeWidth - width) / 2), y: 0};
71 crop = resizeWidth > width;
72 }
73 }
74
75 image
76 .noProfile()
77 .quality(quality)
78 .resizeExact(resizeWidth, resizeHeight);
79
80 if (crop)
81 image.crop(width, height, cropPosition.x, cropPosition.y);
82
83 image.write(thumbnailPath, callback);
84 }
85
86 ], function(error) {
87 callback(error);
88 });
89};
90
91/**
92 * Creates an image from a list of images.
93 *
94 * Input images are aggregated horizontally or vertically to create the new image.
95 *
96 * @method aggregate
97 * @param {Array} imagesPaths The list of paths of the images to add to the final image
98 * @param {String} destinationPath The final image path
99 * @param {Number} width The width of input images inside the image (in px)
100 * @param {Number} height The height of input images inside the image (in px)
101 * @param {Boolean} [horizontally=true] true to aggregate images horizontally, false to aggregate them vertically
102 * @param {Number} [quality=90] Expected quality from 0 to 100 (default to 90 with 100 the best)
103 * @param {String} [temporaryDirectoryPath] Path to the temporary directory to use to store intermediate images. It will
104 * be removed at the end of the operation. If not specified a directory is created in /tmp/
105 * @return {Function} callback Function to call when its done with:
106 * - **Error** An error if something went wrong
107 * - **Array** The list of images with:
108 * - **String** **sprite** The path of the sprite file containing the image (destinationPath)
109 * - **String** **image** The path of the original image
110 * - **Number** **x** The x coordinate of the image top left corner inside the sprite
111 * - **Number** **y** The y coordinate of the image top left corner inside the sprite
112 */
113module.exports.aggregate = function(imagesPaths, destinationPath, width, height, horizontally, quality,
114 temporaryDirectoryPath, callback) {
115 var self = this;
116 var asyncFunctions = [];
117 var thumbnailsPaths = [];
118 var images = [];
119
120 // Validate arguments
121 quality = quality || 90;
122
123 // Use a temporary directory to store thumbnails
124 temporaryDirectoryPath = path.join(temporaryDirectoryPath || path.join(os.tmpdir()), shortid.generate());
125
126 imagesPaths.forEach(function(imagePath) {
127 asyncFunctions.push(function(callback) {
128 var thumbnailPath = path.join(temporaryDirectoryPath, path.basename(imagePath));
129 thumbnailsPaths.push({
130 originalPath: imagePath,
131 thumbnailPath: thumbnailPath
132 });
133
134 self.generateThumbnail(
135 imagePath,
136 thumbnailPath,
137 width,
138 height,
139 true,
140 100,
141 callback
142 );
143 });
144 });
145
146 async.series([
147
148 // Create destination path directory if it does not exist
149 function(callback) {
150 fileSystem.mkdir(path.dirname(destinationPath), function(error) {
151 callback(error);
152 });
153 },
154
155 // Generate thumbnails
156 function(callback) {
157 async.parallel(asyncFunctions, callback);
158 },
159
160 // Aggregate thumbnails
161 function(callback) {
162 var firstThumbnail;
163
164 for (var i = 0; i < thumbnailsPaths.length; i++) {
165 var thumbnailsPath = thumbnailsPaths[i].thumbnailPath;
166
167 if (!firstThumbnail)
168 firstThumbnail = gm(thumbnailsPath);
169 else
170 firstThumbnail.append(thumbnailsPath, horizontally);
171
172 images.push({
173 sprite: destinationPath,
174 image: thumbnailsPaths[i].originalPath,
175 x: horizontally ? width * i : 0,
176 y: horizontally ? 0 : height * i
177 });
178 }
179 firstThumbnail
180 .quality(quality)
181 .write(destinationPath, callback);
182 }
183 ], function(error, results) {
184 fileSystem.rm(temporaryDirectoryPath, function(removeError) {
185 if (error || removeError) return callback(error || removeError);
186
187 callback(null, images);
188 });
189 });
190};
191
192/**
193 * Generates a sprite from a list of images.
194 *
195 * If the number of images exceeds the maximum number of images (depending on totalColumns and maxRows), extra images
196 * won't be in the sprite.
197 *
198 * @method generateSprite
199 * @param {Array} imagesPaths The list of images path to include in the sprite
200 * @param {String} destinationPath The sprite path
201 * @param {Number} width The width of images inside the sprite (in px)
202 * @param {Number} height The height of images inside the sprite (in px)
203 * @param {Number} [totalColumns=5] The number of images per line in the sprite
204 * @param {Number} [maxRows=5] The maximum number of lines of images in the sprite
205 * @param {Number} [quality=90] Expected quality from 0 to 100 (default to 90 with 100 the best)
206 * @param {String} [temporaryDirectoryPath] Path to the temporary directory to use to store intermediate images. It will
207 * be removed at the end of the operation. If not specified a directory is created in /tmp/
208 * @return {Function} callback Function to call when its done with:
209 * - **Error** An error if something went wrong
210 * - **Array** The list of images with:
211 * - **String** **sprite** The path of the sprite file containing the image (destinationPath)
212 * - **String** **image** The path of the original image
213 * - **Number** **x** The x coordinate of the image top left corner inside the sprite
214 * - **Number** **y** The y coordinate of the image top left corner inside the sprite
215 */
216module.exports.generateSprite = function(imagesPaths, destinationPath, width, height, totalColumns, maxRows, quality,
217 temporaryDirectoryPath, callback) {
218 var self = this;
219 var linesPaths = [];
220 var images = [];
221
222 // Validate arguments
223 totalColumns = totalColumns || 5;
224 maxRows = maxRows || 5;
225 quality = quality || 90;
226
227 // Create a copy of the list of images to avoid modifying the original
228 imagesPaths = imagesPaths.slice(0);
229
230 // Use a temporary directory to store intermediate images
231 temporaryDirectoryPath = path.join(temporaryDirectoryPath || path.join(os.tmpdir()), shortid.generate());
232
233 // It is possible to have less than the expected number of columns if not enough images
234 // The number of rows varies depending on the number of columns and the number of images
235 var numberOfColumns = Math.min(imagesPaths.length, totalColumns);
236 var numberOfRows = Math.ceil(imagesPaths.length / numberOfColumns);
237
238 if (numberOfRows > maxRows) {
239
240 // The number of images exceeds the possible number of images implicitly specified by the number of columns and rows
241 // Ignore extra images
242 numberOfRows = maxRows;
243 imagesPaths = imagesPaths.slice(0, numberOfRows * numberOfColumns);
244
245 }
246
247 /**
248 * Creates sprite lines by aggregating images.
249 *
250 * @param {Array} linesImagesPaths The list of images paths to aggregate
251 * @param {String} linePath The path of the image to generate
252 * @param {Number} lineWidth The line width (in px)
253 * @param {Number} lineHeight The line height (in px)
254 * @param {Boolean} horizontally true to create an horizontal line, false to create a vertical line
255 * @param {Number} lineQuality The line quality from 0 to 100 (default to 90 with 100 the best)
256 * @return {Function} The async function of the operation
257 */
258 var createLine = function(linesImagesPaths, linePath, lineWidth, lineHeight, horizontally, lineQuality) {
259 return function(callback) {
260 self.aggregate(
261 linesImagesPaths,
262 linePath,
263 lineWidth,
264 lineHeight,
265 horizontally,
266 lineQuality,
267 temporaryDirectoryPath,
268 callback
269 );
270 };
271 };
272
273 async.series([
274
275 // Create destination path directory if it does not exist
276 function(callback) {
277 fileSystem.mkdir(path.dirname(destinationPath), function(error) {
278 callback(error);
279 });
280 },
281
282 // Create temporary directory if it does not exist
283 function(callback) {
284 fileSystem.mkdir(temporaryDirectoryPath, function(error) {
285 callback(error);
286 });
287 },
288
289 // Complete the grid defined by numberOfColumns and numberOfRows using transparent images if needed
290 function(callback) {
291 if (imagesPaths.length >= numberOfColumns * numberOfRows) return callback();
292
293 var transparentImagePath = path.join(temporaryDirectoryPath, 'transparent.png');
294 gm(width, height, '#00000000').write(transparentImagePath, function(error) {
295
296 // Add as many as needed transparent images to the list of images
297 var totalMissingImages = numberOfColumns * numberOfRows - imagesPaths.length;
298
299 for (var i = 0; i < totalMissingImages; i++)
300 imagesPaths.push(transparentImagePath);
301
302 callback(error);
303 });
304 },
305
306 // Create sprite horizontal lines
307 function(callback) {
308 var asyncFunctions = [];
309
310 for (var i = 0; i < numberOfRows; i++) {
311 var rowsImagesPaths = imagesPaths.slice(i * numberOfColumns, i * numberOfColumns + numberOfColumns);
312 var lineWidth = width;
313 var lineHeight = height;
314 var linePath = path.join(temporaryDirectoryPath, 'line-' + i);
315
316 linesPaths.push(linePath);
317 asyncFunctions.push(createLine(rowsImagesPaths, linePath, lineWidth, lineHeight, true, 100));
318 }
319
320 async.parallel(asyncFunctions, function(error, results) {
321 if (error) return callback(error);
322
323 results.forEach(function(line) {
324 line.forEach(function(image) {
325 if (image.image === path.join(temporaryDirectoryPath, 'transparent.png')) return;
326
327 var spritePathChunks = path.parse(image.sprite).name.match(/-([0-9]+)$/);
328 var lineIndex = (spritePathChunks && parseInt(spritePathChunks[1])) || 0;
329
330 image.y = image.y + (lineIndex * height);
331 image.sprite = destinationPath;
332 images.push(image);
333 });
334 });
335
336 callback();
337 });
338 },
339
340 // Aggregate lines vertically
341 function(callback) {
342 createLine(linesPaths, destinationPath, width * numberOfColumns, height, false, quality)(callback);
343 }
344
345 ], function(error, results) {
346 fileSystem.rm(temporaryDirectoryPath, function(removeError) {
347 if (error || removeError) return callback(error || removeError);
348
349 callback(null, images);
350 });
351 });
352};
353
354/**
355 * Generates sprites from a list of images.
356 *
357 * If the number of images don't fit in the grid defined by totalColumns * maxRows, then several sprites will be
358 * created.
359 * Additional sprites are suffixed by a number.
360 *
361 * @method generateSprites
362 * @param {Array} imagesPaths The list of images paths to include in the sprites
363 * @param {String} destinationPath The first sprite path, additional sprites are suffixed by a number
364 * @param {Number} width The width of images inside the sprite (in px)
365 * @param {Number} height The height of images inside the sprite (in px)
366 * @param {Number} [totalColumns=5] The number of images per line in the sprite
367 * @param {Number} [maxRows=5] The maximum number of lines of images in the sprite
368 * @param {Number} [quality=90] Expected quality from 0 to 100 (default to 90 with 100 the best)
369 * @param {String} [temporaryDirectoryPath] Path to the temporary directory to use to store intermediate images. It
370 * will be removed at the end of the operation. If not specified a directory is created in /tmp/
371 * @return {Function} callback Function to call when its done with:
372 * - **Error** An error if something went wrong
373 * - **Array** The list of images with:
374 * - **String** **sprite** The path of the sprite file containing the image
375 * - **String** **image** The path of the original image
376 * - **Number** **x** The x coordinate of the image top left corner inside the sprite
377 * - **Number** **y** The y coordinate of the image top left corner inside the sprite
378 */
379module.exports.generateSprites = function(imagesPaths, destinationPath, width, height, totalColumns, maxRows, quality,
380 temporaryDirectoryPath, callback) {
381 var self = this;
382 var asyncFunctions = [];
383
384 // Validate arguments
385 totalColumns = totalColumns || 5;
386 maxRows = maxRows || 5;
387 temporaryDirectoryPath = path.join(temporaryDirectoryPath || path.join(os.tmpdir()), shortid.generate());
388
389 // Find out how many sprites that have to be created
390 var spriteMaxImages = totalColumns * maxRows;
391 var totalSprites = Math.ceil(imagesPaths.length / spriteMaxImages);
392
393 /**
394 * Creates a sprite.
395 *
396 * @param {Array} spriteImagesPaths The list of images to include in the sprite
397 * @param {String} spriteDestinationPath The sprite path
398 * @return {Function} The async function of the operation
399 */
400 var createSprite = function(spriteImagesPaths, spriteDestinationPath) {
401 return function(callback) {
402 self.generateSprite(
403 spriteImagesPaths,
404 spriteDestinationPath,
405 width,
406 height,
407 totalColumns,
408 maxRows,
409 quality,
410 temporaryDirectoryPath,
411 callback
412 );
413 };
414 };
415
416 for (var i = 0; i < totalSprites; i++) {
417 var spriteImagesPaths = imagesPaths.slice(i * spriteMaxImages, i * spriteMaxImages + spriteMaxImages);
418 var spriteDestinationPath = destinationPath;
419
420 if (i > 0) {
421 var destinationPathChunks = path.parse(destinationPath);
422 destinationPathChunks.base = destinationPathChunks.name + '-' + i + destinationPathChunks.ext;
423 spriteDestinationPath = path.format(destinationPathChunks);
424 }
425
426 asyncFunctions.push(createSprite(spriteImagesPaths, spriteDestinationPath));
427 }
428
429 async.parallel(asyncFunctions, function(error, results) {
430 fileSystem.rm(temporaryDirectoryPath, function(removeError) {
431 if (error || removeError) return callback(error || removeError);
432
433 var images = [];
434 results.forEach(function(sprite) {
435 images = images.concat(sprite);
436 });
437
438 callback(null, images);
439 });
440 });
441};