<commit_before>/*!
 * Copyright 2012 Sakai Foundation (SF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://www.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var fs = require('fs');
var gm = require('gm');
var Path = require('path');
var temp = require('temp');

var log = require('oae-logger').logger('Image');
var Validator = require('oae-util/lib/validator').Validator;


/**
 * Crops and resizes an image.
 *
 * @param {String}      path                    The path on disk of the image to crop.
 * @param {Number}      selectedArea.x          The x coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.y          The y coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.width      The width of the square that needs to be cropped out.
 * @param {Number[]}    sizes                   An array of image sizes. An image will be generated for each size. You only need to specify the width for each size as this method only generates squared images.
 * @param {Function}    callback                Standard callback function.
 * @param {Object}      callback.err            An error object (if any.)
 * @param {Object}      callback.files          An object where each entry holds a resized file. The keys are the requested sizes.
 */
var cropAndResize = module.exports.cropAndResize = function(path, selectedArea, sizes, callback) {
    var validator = new Validator();
    validator.check(path, {'code': 400, 'msg': 'A path to the image that you want to crop is missing'}).notNull();
    validator.check(null, {'code': 400, 'msg': 'The coordinates for the area you wish to crop must be specified'}).isObject(selectedArea);
    if (selectedArea) {
        validator.check(selectedArea.x, {'code': 400, 'msg': 'The x-coordinate needs to be a valid integer'}).isInt();
        validator.check(selectedArea.x, {'code': 400, 'msg': 'The x-coordinate needs to be a valid integer'}).min(0);
        validator.check(selectedArea.y, {'code': 400, 'msg': 'The y-coordinate needs to be a valid integer'}).isInt();
        validator.check(selectedArea.y, {'code': 400, 'msg': 'The y-coordinate needs to be a valid integer'}).min(0);
        validator.check(selectedArea.width, {'code': 400, 'msg': 'The width value must be an integer larger than 0'}).isInt();
        validator.check(selectedArea.width, {'code': 400, 'msg': 'The width value must be an integer larger than 0'}).min(1);
    }
    validator.check(sizes, {'code': 400, 'msg': 'The desired sizes array is missing'}).notNull();
    if (sizes) {
        validator.check(sizes.length, {'code': 400, 'msg': 'The desired sizes array is empty'}).min(1);
        for (var i = 0; i < sizes.length; i++) {
            validator.check(sizes[i], {'code': 400, 'msg': 'The size needs to be a valid integer larger than 0'}).isInt();
            validator.check(sizes[i], {'code': 400, 'msg': 'The size needs to be a valid integer larger than 0'}).min(0);
        }
    }
    if (validator.hasErrors()) {
        return callback(validator.getFirstError());
    }

    // Crop the image.
    _cropImage(path, selectedArea, function(err, croppedFile) {
        if (err) {
            return callback(err);
        }

        var files = {};
        var resized = 0;
        var called = false;

        // Use a foreach so that the callback function of resizeImage has the size on the stack.
        sizes.forEach(function(size) {

            // Resize the image.
            _resizeImage(croppedFile.path, {'width': size, 'height': size}, function(err, file) {
                resized++;
                if (err && !called) {
                    called = true;
                    return callback(err);
                }

                files[size] = file;
                if (resized === sizes.length && !called) {
                    called = true;

                    // Remove the cropped one before we call the callback.
                    fs.unlink(croppedFile.path, function(err) {
                        if (err) {
                            called = true;
                            return callback({'code': 500, 'msg': err});
                        }
                        callback(null, files);
                    });
                }
            });
        });
    });
};

/**
 * Crops a part out of an image.
 *
 * @param {String}      path                    The path on disk of the file that needs to be cropped.
 * @param {Object}      selectedArea            The area that needs to be cropped out.
 * @param {Number}      selectedArea.x          The x coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.y          The y coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.width      The width of the box that needs to be cropped out.
 * @param {Number}      selectedArea.height     The height of the box that needs to be cropped out.
 * @param {Function}    callback                Standard callback function.
 * @param {Object}      callback.err            An error object (if any.)
 * @param {Object}      callback.file           A file object with some metadata of the cropped file.
 * @param {Object}      callback.file.path      The path where the file has been written to.
 * @param {Object}      callback.file.name      The name of the file
 * @param {Object}      callback.file.size      The size of the cropped image (in bytes).
 */
var cropImage = module.exports.cropImage = function(path, selectedArea, callback) {
    var validator = new Validator();
    validator.check(path, {'code': 400, 'msg': 'A path to the image that you want to crop is missing'}).notNull();
    validator.check(null, {'code': 400, 'msg': 'The coordinates for the area you wish to crop must be specified'}).isObject(selectedArea);
    if (selectedArea) {
        validator.check(selectedArea.x, {'code': 400, 'msg': 'The x-coordinate needs to be a valid integer'}).isInt();
        validator.check(selectedArea.x, {'code': 400, 'msg': 'The x-coordinate needs to be a valid integer'}).min(0);
        validator.check(selectedArea.y, {'code': 400, 'msg': 'The y-coordinate needs to be a valid integer'}).isInt();
        validator.check(selectedArea.y, {'code': 400, 'msg': 'The y-coordinate needs to be a valid integer'}).min(0);
        validator.check(selectedArea.width, {'code': 400, 'msg': 'The width value must be an integer larger than 0'}).isInt();
        validator.check(selectedArea.width, {'code': 400, 'msg': 'The width value must be an integer larger than 0'}).min(1);
    }
    if (validator.hasErrors()) {
        return callback(validator.getFirstError());
    }
    _cropImage(path, selectedArea, callback);
};

/**
 * Internal method that performs the actual cropping.
 * This does no validation of the parameters.
 *
 * @param {String}      path                    The path on disk of the file that needs to be cropped.
 * @param {Object}      selectedArea            The area that needs to be cropped out.
 * @param {Number}      selectedArea.x          The x coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.y          The y coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.width      The width of the box that needs to be cropped out.
 * @param {Number}      selectedArea.height     The height of the box that needs to be cropped out.
 * @param {Function}    callback                Standard callback function.
 * @param {Object}      callback.err            An error object (if any.)
 * @param {Object}      callback.file           A file object with some metadata of the cropped file.
 * @param {Object}      callback.file.path      The path where the file has been written to.
 * @param {Object}      callback.file.name      The name of the file
 * @param {Object}      callback.file.size      The size of the cropped image (in bytes).
 * @api private
 */
var _cropImage = function(path, selectedArea, callback) {
    // Make sure that the pic is big enough.
    gm(path).size(function (err, size) {
        if (err) {
            log().error({'err': err}, 'Could not get the image size for the large image.');
            return callback({'code': 500, 'msg': 'Could not get the image size for the large image.'});
        }
        // A user can crop out a 1x1 pixel,
        // cropping outside the image is not allowed though.
        if (selectedArea.x + 1 >= size.width
            || selectedArea.y + 1 >= size.height
            || selectedArea.width > (size.width - selectedArea.x)
            || selectedArea.width > (size.height - selectedArea.y)) {
            return callback({'code': 400, 'msg': 'You cannot crop outside of the image.'});
        }

        // Crop it and write it to a temporary file.
        var tempPath = './gm/' + Path.basename(path) + '.cropped.png';
        gm(path)
        .noProfile()
        .crop(selectedArea.width, selectedArea.width, selectedArea.x, selectedArea.y)
        .noProfile()
        .write(tempPath, function(err) {
            if (err) {
                fs.unlink(tempPath);
                log().error({'err': err}, 'Could not crop the image %s', path);
                return callback({'code': 500, 'msg': 'Could not crop the image.'});
            }
            fs.stat(tempPath, function(err, stat) {
                if (err) {
                    fs.unlink(tempPath);
                    log().error({'err': err}, 'Could not get the file system information about %s', tempPath);
                    return callback({'code': 500, 'msg': 'Could not retrieve the file information for the cropped file.'});
                }
                var file = {
                    'path': tempPath,
                    'size': stat.size,
                    'name': Path.basename(tempPath)
                };
                callback(null, file);
            });
        });
    });
};

/**
 * Resizes an image to the specified size.
 *
 * @param {String}      path                    The path on disk of the file that needs to be resized.<commit_msg>Removed wrong tempPath.<commit_after>/*!
 * Copyright 2012 Sakai Foundation (SF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://www.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var fs = require('fs');
var gm = require('gm');
var Path = require('path');
var temp = require('temp');

var log = require('oae-logger').logger('Image');
var Validator = require('oae-util/lib/validator').Validator;


/**
 * Crops and resizes an image.
 *
 * @param {String}      path                    The path on disk of the image to crop.
 * @param {Number}      selectedArea.x          The x coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.y          The y coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.width      The width of the square that needs to be cropped out.
 * @param {Number[]}    sizes                   An array of image sizes. An image will be generated for each size. You only need to specify the width for each size as this method only generates squared images.
 * @param {Function}    callback                Standard callback function.
 * @param {Object}      callback.err            An error object (if any.)
 * @param {Object}      callback.files          An object where each entry holds a resized file. The keys are the requested sizes.
 */
var cropAndResize = module.exports.cropAndResize = function(path, selectedArea, sizes, callback) {
    var validator = new Validator();
    validator.check(path, {'code': 400, 'msg': 'A path to the image that you want to crop is missing'}).notNull();
    validator.check(null, {'code': 400, 'msg': 'The coordinates for the area you wish to crop must be specified'}).isObject(selectedArea);
    if (selectedArea) {
        validator.check(selectedArea.x, {'code': 400, 'msg': 'The x-coordinate needs to be a valid integer'}).isInt();
        validator.check(selectedArea.x, {'code': 400, 'msg': 'The x-coordinate needs to be a valid integer'}).min(0);
        validator.check(selectedArea.y, {'code': 400, 'msg': 'The y-coordinate needs to be a valid integer'}).isInt();
        validator.check(selectedArea.y, {'code': 400, 'msg': 'The y-coordinate needs to be a valid integer'}).min(0);
        validator.check(selectedArea.width, {'code': 400, 'msg': 'The width value must be an integer larger than 0'}).isInt();
        validator.check(selectedArea.width, {'code': 400, 'msg': 'The width value must be an integer larger than 0'}).min(1);
    }
    validator.check(sizes, {'code': 400, 'msg': 'The desired sizes array is missing'}).notNull();
    if (sizes) {
        validator.check(sizes.length, {'code': 400, 'msg': 'The desired sizes array is empty'}).min(1);
        for (var i = 0; i < sizes.length; i++) {
            validator.check(sizes[i], {'code': 400, 'msg': 'The size needs to be a valid integer larger than 0'}).isInt();
            validator.check(sizes[i], {'code': 400, 'msg': 'The size needs to be a valid integer larger than 0'}).min(0);
        }
    }
    if (validator.hasErrors()) {
        return callback(validator.getFirstError());
    }

    // Crop the image.
    _cropImage(path, selectedArea, function(err, croppedFile) {
        if (err) {
            return callback(err);
        }

        var files = {};
        var resized = 0;
        var called = false;

        // Use a foreach so that the callback function of resizeImage has the size on the stack.
        sizes.forEach(function(size) {

            // Resize the image.
            _resizeImage(croppedFile.path, {'width': size, 'height': size}, function(err, file) {
                resized++;
                if (err && !called) {
                    called = true;
                    return callback(err);
                }

                files[size] = file;
                if (resized === sizes.length && !called) {
                    called = true;

                    // Remove the cropped one before we call the callback.
                    fs.unlink(croppedFile.path, function(err) {
                        if (err) {
                            called = true;
                            return callback({'code': 500, 'msg': err});
                        }
                        callback(null, files);
                    });
                }
            });
        });
    });
};

/**
 * Crops a part out of an image.
 *
 * @param {String}      path                    The path on disk of the file that needs to be cropped.
 * @param {Object}      selectedArea            The area that needs to be cropped out.
 * @param {Number}      selectedArea.x          The x coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.y          The y coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.width      The width of the box that needs to be cropped out.
 * @param {Number}      selectedArea.height     The height of the box that needs to be cropped out.
 * @param {Function}    callback                Standard callback function.
 * @param {Object}      callback.err            An error object (if any.)
 * @param {Object}      callback.file           A file object with some metadata of the cropped file.
 * @param {Object}      callback.file.path      The path where the file has been written to.
 * @param {Object}      callback.file.name      The name of the file
 * @param {Object}      callback.file.size      The size of the cropped image (in bytes).
 */
var cropImage = module.exports.cropImage = function(path, selectedArea, callback) {
    var validator = new Validator();
    validator.check(path, {'code': 400, 'msg': 'A path to the image that you want to crop is missing'}).notNull();
    validator.check(null, {'code': 400, 'msg': 'The coordinates for the area you wish to crop must be specified'}).isObject(selectedArea);
    if (selectedArea) {
        validator.check(selectedArea.x, {'code': 400, 'msg': 'The x-coordinate needs to be a valid integer'}).isInt();
        validator.check(selectedArea.x, {'code': 400, 'msg': 'The x-coordinate needs to be a valid integer'}).min(0);
        validator.check(selectedArea.y, {'code': 400, 'msg': 'The y-coordinate needs to be a valid integer'}).isInt();
        validator.check(selectedArea.y, {'code': 400, 'msg': 'The y-coordinate needs to be a valid integer'}).min(0);
        validator.check(selectedArea.width, {'code': 400, 'msg': 'The width value must be an integer larger than 0'}).isInt();
        validator.check(selectedArea.width, {'code': 400, 'msg': 'The width value must be an integer larger than 0'}).min(1);
    }
    if (validator.hasErrors()) {
        return callback(validator.getFirstError());
    }
    _cropImage(path, selectedArea, callback);
};

/**
 * Internal method that performs the actual cropping.
 * This does no validation of the parameters.
 *
 * @param {String}      path                    The path on disk of the file that needs to be cropped.
 * @param {Object}      selectedArea            The area that needs to be cropped out.
 * @param {Number}      selectedArea.x          The x coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.y          The y coordinate of the topleft corner to start cropping
 * @param {Number}      selectedArea.width      The width of the box that needs to be cropped out.
 * @param {Number}      selectedArea.height     The height of the box that needs to be cropped out.
 * @param {Function}    callback                Standard callback function.
 * @param {Object}      callback.err            An error object (if any.)
 * @param {Object}      callback.file           A file object with some metadata of the cropped file.
 * @param {Object}      callback.file.path      The path where the file has been written to.
 * @param {Object}      callback.file.name      The name of the file
 * @param {Object}      callback.file.size      The size of the cropped image (in bytes).
 * @api private
 */
var _cropImage = function(path, selectedArea, callback) {
    // Make sure that the pic is big enough.
    gm(path).size(function (err, size) {
        if (err) {
            log().error({'err': err}, 'Could not get the image size for the large image.');
            return callback({'code': 500, 'msg': 'Could not get the image size for the large image.'});
        }
        // A user can crop out a 1x1 pixel,
        // cropping outside the image is not allowed though.
        if (selectedArea.x + 1 >= size.width
            || selectedArea.y + 1 >= size.height
            || selectedArea.width > (size.width - selectedArea.x)
            || selectedArea.width > (size.height - selectedArea.y)) {
            return callback({'code': 400, 'msg': 'You cannot crop outside of the image.'});
        }

        // Crop it and write it to a temporary file.
        var tempPath = temp.path({'suffix': '.png'});
        gm(path)
        .noProfile()
        .crop(selectedArea.width, selectedArea.width, selectedArea.x, selectedArea.y)
        .noProfile()
        .write(tempPath, function(err) {
            if (err) {
                fs.unlink(tempPath);
                log().error({'err': err}, 'Could not crop the image %s', path);
                return callback({'code': 500, 'msg': 'Could not crop the image.'});
            }
            fs.stat(tempPath, function(err, stat) {
                if (err) {
                    fs.unlink(tempPath);
                    log().error({'err': err}, 'Could not get the file system information about %s', tempPath);
                    return callback({'code': 500, 'msg': 'Could not retrieve the file information for the cropped file.'});
                }
                var file = {
                    'path': tempPath,
                    'size': stat.size,
                    'name': Path.basename(tempPath)
                };
                callback(null, file);
            });
        });
    });
};

/**
 * Resizes an image to the specified size.
 *
 * @param {String}      path                    The path on disk of the file that needs to be resized.
{'fraction_non_alphanumeric': 0.11473161503901631, 'fraction_numerical': 0.009742255852447387, 'mean_word_length': 2.2497310588596897, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1192523', 'n_tokens_mistral': 5907, 'n_tokens_neox': 5624, 'n_words': 2224}