├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── lib ├── config.js ├── format.js ├── generate.js ├── index.js └── size.js ├── package.json └── test ├── images └── aileen.jpg └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Output of 'npm pack' 44 | *.tgz 45 | 46 | # Yarn Integrity file 47 | .yarn-integrity 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | - '6' 5 | sudo: false 6 | addons: 7 | apt: 8 | sources: 9 | - ubuntu-toolchain-r-test 10 | packages: 11 | - g++-4.8 12 | env: CXX=g++-4.8 NODE_ENV=test -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Code from responsive-images-generator: 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2016 Felix Rieseberg 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | Code taken from gulp-responsive: 26 | 27 | The MIT License (MIT) 28 | 29 | Copyright (c) 2014-2016 Evgeny Vlasenko 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all 39 | copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 47 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Responsive Images Generator 2 | [![Build Status](https://travis-ci.org/felixrieseberg/responsive-images-generator.svg?branch=master)](https://travis-ci.org/felixrieseberg/responsive-images-generator) 3 | Let's say you need to generate some responsive images. Automatically. This package will help you do it. Greatly inspired by [gulp-responsive](https://github.com/mahnunchik/gulp-responsive), which reduced the time it took to build this down to a mere hours. The configuration object is basically the same. If you're using Gulp, just go and use it. 4 | 5 | ``` 6 | npm install responsive-images-generator 7 | ``` 8 | 9 | ### Usage Example 10 | 11 | #### Simple Scaling 12 | Let's say you have two images `aileen.jpg` and `kevin.jpg` and want said images to be resized. 13 | 14 | ``` 15 | const configs = [ 16 | {width: '20%', rename: {suffix: '@1x'}}, 17 | {width: '40%', rename: {suffix: '@2x'}}, 18 | {width: '60%', rename: {suffix: '@3x'}}, 19 | {width: '80%', rename: {suffix: '@4x'}}, 20 | {width: '100%', rename: {suffix: '@5x'}} 21 | ] 22 | const images = [ 23 | path.join(__dirname, 'aileen.jpg'), 24 | path.join(__dirname, 'kevin.jpg') 25 | ] 26 | 27 | generateResponsiveImages(images, configs) 28 | ``` 29 | 30 | :mag: Output on disk will be: 31 | ``` 32 | aileen.jpg 33 | aileen@1x.jpg 34 | aileen@2x.jpg 35 | aileen@3x.jpg 36 | aileen@4x.jpg 37 | aileen@5x.jpg 38 | kevin.jpg 39 | kevin@1x.jpg 40 | kevin@2x.jpg 41 | kevin@3x.jpg 42 | kevin@4x.jpg 43 | kevin@5x.jpg 44 | ``` 45 | 46 | ### Renaming Images To Width 47 | If you want to use your scaled images with a `srcset` (or something similar), you might need 48 | to rename your images sensibly. Let's go and do that. 49 | 50 | ``` 51 | const pattern = /(?:.*)(@[0-9]{0,10}x)$/ 52 | const files = fs.readdirSync('/path/to/my/images') 53 | .filter((file) => file !== '.DS_Store') 54 | .map((file) => `/path/to/my/images/${file}`) 55 | 56 | renameImagesToSize(files, pattern) 57 | ``` 58 | 59 | :mag: Output on disk will be: 60 | ``` 61 | aileen-120x.jpg 62 | aileen-180x.jpg 63 | aileen-240x.jpg 64 | aileen-300x.jpg 65 | aileen-60x.jpg 66 | aileen.jpg 67 | kevin-120x.jpg 68 | kevin-180x.jpg 69 | kevin-240x.jpg 70 | kevin-300x.jpg 71 | kevin-60x.jpg 72 | kevin.jpg 73 | ``` 74 | 75 | ### Configuration 76 | 77 | Configuration unit is an object: 78 | 79 | * **name**: *String* — filename glob pattern. 80 | * **width**: *Number* or *String* — width in pixels or percentage of the original, not set by default. 81 | * **height**: *Number* or *String* — height in pixels or percentage of the original, not set by default. 82 | * [**withoutEnlargement**](http://sharp.dimens.io/en/stable/api/#withoutenlargement): *Boolean* — do not enlarge the output image, default `true`. 83 | * **skipOnEnlargement**: *Boolean* — do not write an output image at all if the original image is smaller than the configured width or height, default `false`. 84 | * [**min**](http://sharp.dimens.io/en/stable/api/#min): *Boolean* — preserving aspect ratio, resize the image to be as small as possible while ensuring its dimensions are greater than or equal to the `width` and `height` specified. 85 | * [**max**](http://sharp.dimens.io/en/stable/api/#max): *Boolean* — resize to the max width or height the preserving aspect ratio (both `width` and `height` have to be defined), default `false`. 86 | * [**quality**](http://sharp.dimens.io/en/stable/api/#qualityquality): *Number* — output quality for JPEG, WebP and TIFF, default `80`. 87 | * [**progressive**](http://sharp.dimens.io/en/stable/api/#progressive): *Boolean* — progressive (interlace) scan for JPEG and PNG output, default `false`. 88 | * [**withMetadata**](http://sharp.dimens.io/en/stable/api/#withmetadatametadata): *Boolean* — include image metadata, default `false`. 89 | * [**compressionLevel**](http://sharp.dimens.io/en/stable/api/#compressionlevelcompressionlevel): *Number* — zlib compression level for PNG, default `6`. 90 | * [**rename**](#renaming): *String*, *Object* or *Function* — renaming options, file will not be renamed by default. When `extname` is specified, output format is parsed from extension. You can override this autodetection with `format` option. 91 | * [**format**](http://sharp.dimens.io/en/stable/api/#toformatformat): *String* — output format `jpeg`, `png`, `webp` or `raw`, default is `null`. 92 | * [**crop**](http://sharp.dimens.io/en/stable/api/#cropgravity): Crop the resized image to the exact size specified, default is `false`. 93 | * [**embed**](http://sharp.dimens.io/en/stable/api/#embed): Preserving aspect ratio, resize the image to the maximum `width` or `height` specified then `embed` on a `background` of the exact `width` and `height` specified, default is `false`. 94 | * [**ignoreAspectRatio**](http://sharp.dimens.io/en/stable/api/#ignoreaspectratio): *Boolean* — Ignoring the aspect ratio of the input, stretch the image to the exact `width` and/or `height` provided via `resize`, default is `false`. 95 | * [**interpolator**](http://sharp.dimens.io/en/stable/api/#resizewidth-height-options): *String* — The interpolator to use for image **enlargement**, defaulting to `bicubic`. 96 | * [**kernel**](http://sharp.dimens.io/en/stable/api/#resizewidth-height-options): *String* — The kernel to use for image **reduction**, defaulting to `lanczos3`. 97 | * [**background**](http://sharp.dimens.io/en/stable/api/#backgroundrgba): [*Color*](https://www.npmjs.com/package/color) — Set the background for the embed and flatten operations, '#default is `fff`'. 98 | * [**flatten**](http://sharp.dimens.io/en/stable/api/#flatten): *Boolean* — Merge alpha transparency channel, if any, with `background`, default is `false`. 99 | * [**negate**](http://sharp.dimens.io/en/stable/api/#negate): *Boolean* — Produces the "negative" of the image, default is `false`. 100 | * [**rotate**](http://sharp.dimens.io/en/stable/api/#rotateangle): *Boolean* — Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag, default is `false`. 101 | * [**flip**](http://sharp.dimens.io/en/stable/api/#flip): *Boolean* — Flip the image about the vertical Y axis. This always occurs after rotation, if any. The use of `flip` implies the removal of the EXIF `Orientation` tag, if any. Default is `false`. 102 | * [**flop**](http://sharp.dimens.io/en/stable/api/#flop): *Boolean* — Flop the image about the horizontal X axis. This always occurs after rotation, if any. The use of `flop` implies the removal of the EXIF `Orientation` tag, if any. Default is `false`. 103 | * [**blur**](http://sharp.dimens.io/en/stable/api/#blursigma): *Boolean* — When used without parameters, performs a fast, mild blur of the output image. This typically reduces performance by 10%. Default is `false`. 104 | * [**sharpen**](http://sharp.dimens.io/en/stable/api/#sharpensigma-flat-jagged): *Boolean* — When used without parameters, performs a fast, mild sharpen of the output image. This typically reduces performance by 10%. Default is `false`. 105 | * [**threshold**](http://sharp.dimens.io/en/stable/api/#thresholdthreshold): *Number* or *Boolean* — Converts all pixels in the image to greyscale white or black, default is `false`. 106 | * [**gamma**](http://sharp.dimens.io/en/stable/api/#gammagamma): *Boolean* — Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma` then increasing the encoding (brighten) post-resize at a factor of `gamma`. Default is `false`. 107 | * [**grayscale**](http://sharp.dimens.io/en/stable/api/#grayscale-greyscale): *Boolean* — Convert to 8-bit greyscale; 256 shades of grey, default is `false`. 108 | * [**normalize**](http://sharp.dimens.io/en/stable/api/#normalize-normalise): *Boolean* — Enhance output image contrast by stretching its luminance to cover the full dynamic range. This typically reduces performance by 30%. Default is `false`. 109 | * [**tile**](http://sharp.dimens.io/en/stable/api/#tileoptions): *Boolean* or *Object* — The size and overlap, in pixels, of square Deep Zoom image pyramid tiles, default is `false`. 110 | * [**withoutChromaSubsampling**](http://sharp.dimens.io/en/stable/api/#withoutchromasubsampling): *Boolean* — Disable the use of [chroma subsampling](http://en.wikipedia.org/wiki/Chroma_subsampling) with JPEG output (4:4:4), default is `false`. 111 | 112 | Detailed description of each option can be found in the [sharp API documentation](http://sharp.dimens.io/en/stable/api/). 113 | 114 | ##### Renaming 115 | Renaming is implemented by the [rename](https://www.npmjs.com/package/rename) module. Options correspond with options of [gulp-rename](https://www.npmjs.com/package/gulp-rename). 116 | 117 | ## License 118 | MIT, Please see license for details. 119 | Code taken from gulp-responsive MIT © [Evgeny Vlasenko](https://github.com/mahnunchik) -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function mergeConfig (config) { 4 | var defaultConfig = { 5 | crop: false, 6 | embed: false, 7 | min: false, 8 | max: false, 9 | withoutEnlargement: true, 10 | skipOnEnlargement: false, 11 | ignoreAspectRatio: false, 12 | interpolator: 'bicubic', 13 | kernel: 'lanczos3', 14 | extractBeforeResize: false, 15 | extractAfterResize: false, 16 | background: '#fff', 17 | flatten: false, 18 | negate: false, 19 | rotate: false, 20 | flip: false, 21 | flop: false, 22 | blur: false, 23 | sharpen: false, 24 | threshold: false, 25 | gamma: false, 26 | grayscale: false, 27 | normalize: false, 28 | quality: 80, 29 | progressive: false, 30 | withMetadata: false, 31 | tile: false, 32 | withoutChromaSubsampling: false, 33 | compressionLevel: 6, 34 | format: null 35 | } 36 | 37 | return Object.assign({}, defaultConfig, config) 38 | } 39 | 40 | module.exports = mergeConfig 41 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | module.exports = function (filePath) { 6 | const extname = path.extname(filePath) 7 | 8 | switch (extname) { 9 | case '.jpeg': 10 | case '.jpg': 11 | case '.jpe': 12 | return 'jpeg' 13 | case '.png': 14 | return 'png' 15 | case '.webp': 16 | return 'webp' 17 | default: 18 | return 'unsupported' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/generate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sharp = require('sharp') 4 | const path = require('path') 5 | const rename = require('rename') 6 | const size = require('./size') 7 | const format = require('./format') 8 | const debug = require('debug')('responsive-images-creator') 9 | 10 | function generateResponsiveImage (filePath, config, options) { 11 | return new Promise((resolve, reject) => { 12 | const msgPrefix = `File ${filePath}: ` 13 | const image = sharp(filePath) 14 | const filePathObj = path.parse(filePath) 15 | 16 | image.metadata((err, metadata) => { 17 | if (err) return reject(err) 18 | 19 | let width, height, extract, toFormat 20 | 21 | if (config.rename) { 22 | filePath = path.join(filePathObj.base, rename(path.relative(filePathObj.base, filePath), config.rename)) 23 | } 24 | 25 | if (config.format) { 26 | toFormat = config.format 27 | } else { 28 | toFormat = format(filePath) 29 | } 30 | 31 | try { 32 | width = size(config.width, metadata.width) 33 | height = size(config.height, metadata.height) 34 | } catch (err) { 35 | return reject(err) 36 | } 37 | 38 | if (width || height) { 39 | if (config.withoutEnlargement && (width > metadata.width || height > metadata.height)) { 40 | let message = `${msgPrefix} Image enlargement is detected` 41 | 42 | if (width) { 43 | message += `\n real width: ${metadata.width}px, required width: ${width}px` 44 | } 45 | 46 | if (height) { 47 | message += `\n real height: ${metadata.height}px, required height: ${height}px` 48 | } 49 | 50 | if (options.errorOnEnlargement) { 51 | reject(message) 52 | } else if (config.skipOnEnlargement) { 53 | if (!options.silent) { 54 | debug(`${msgPrefix} : (skip for processing)`) 55 | } 56 | 57 | // passing a null file to the callback stops a new image being added to the pipeline for this config 58 | return resolve(null) 59 | } 60 | 61 | if (!options.silent) { 62 | debug(`${msgPrefix} : (skip for enlargement)`) 63 | } 64 | } 65 | } 66 | 67 | try { 68 | if (config.extractBeforeResize) { 69 | extract = config.extractBeforeResize 70 | image.extract(extract.top, extract.left, extract.width, extract.height) 71 | } 72 | 73 | image.resize(width, height, { 74 | interpolator: config.interpolator, 75 | kernel: config.kernel 76 | }) 77 | 78 | if (config.extractAfterResize) { 79 | extract = config.extractAfterResize 80 | image.extract(extract.top, extract.left, extract.width, extract.height) 81 | } 82 | 83 | if (config.crop !== false) { 84 | image.crop(config.crop) 85 | } 86 | 87 | if (config.embed) { 88 | image.embed() 89 | } 90 | 91 | if (config.max) { 92 | image.max() 93 | } 94 | 95 | if (config.min) { 96 | image.min() 97 | } 98 | 99 | if (config.ignoreAspectRatio) { 100 | image.ignoreAspectRatio() 101 | } 102 | 103 | image.withoutEnlargement(config.withoutEnlargement) 104 | image.background(config.background) 105 | image.flatten(config.flatten) 106 | image.negate(config.negate) 107 | 108 | if (config.rotate !== false) { 109 | if (typeof config.rotate === 'boolean') { 110 | image.rotate() 111 | } else { 112 | image.rotate(config.rotate) 113 | } 114 | } 115 | 116 | image.flip(config.flip) 117 | image.flop(config.flop) 118 | image.blur(config.blur) 119 | 120 | if (typeof config.sharpen === 'boolean') { 121 | image.sharpen(config.sharpen) 122 | } else { 123 | image.sharpen(config.sharpen.sigma, config.sharpen.flat, config.sharpen.jagged) 124 | } 125 | 126 | image.threshold(config.threshold) 127 | 128 | if (config.gamma !== false) { 129 | if (typeof config.gamma === 'boolean') { 130 | image.gamma() 131 | } else { 132 | image.gamma(config.gamma) 133 | } 134 | } 135 | 136 | image.grayscale(config.grayscale) 137 | image.normalize(config.normalize) 138 | image.quality(config.quality) 139 | image.progressive(config.progressive) 140 | image.withMetadata(config.withMetadata) 141 | image.tile(config.tile) 142 | image.withoutChromaSubsampling(config.withoutChromaSubsampling) 143 | image.compressionLevel(config.compressionLevel) 144 | 145 | image.toFormat(toFormat) 146 | } catch (err) { 147 | err.file = filePath 148 | return reject(err) 149 | } 150 | 151 | image.toFile(filePath, (err) => { 152 | if (err) { 153 | return reject(err) 154 | } else { 155 | return resolve() 156 | } 157 | }) 158 | }) 159 | }) 160 | } 161 | 162 | module.exports = generateResponsiveImage 163 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sharp = require('sharp') 4 | const path = require('path') 5 | const fs = require('fs') 6 | const debug = require('debug')('responsive-images-creator') 7 | 8 | const mergeConfig = require('./config') 9 | const generate = require('./generate') 10 | 11 | /** 12 | * Generates responsive images for a list of files, 13 | * according to specified configuration and options 14 | * 15 | * @param {Array} files 16 | * @param {Object} configs 17 | * @param {Object} options 18 | * @returns {Promise} 19 | */ 20 | function generateResponsiveImages (files, configs, options) { 21 | const promises = [] 22 | 23 | files.forEach((file) => { 24 | configs.forEach((config) => { 25 | promises.push(generate(file, mergeConfig(config), options)) 26 | }) 27 | }) 28 | 29 | return Promise.all(promises) 30 | } 31 | 32 | /** 33 | * Gets all files in a directory that are scaled 34 | * (aka named @2x, @3x, @4x, etc) 35 | * 36 | * @param {String} directory 37 | * @returns {Promise} 38 | */ 39 | function getResponsiveImages (directory) { 40 | return new Promise((resolve, reject) => { 41 | const nameMatch = /(?:.*)(@[0-9]{0,10}x)\.[A-Za-z]{0,5}$/ 42 | 43 | fs.readdir(directory, (err, files) => { 44 | if (err) { 45 | return reject(err) 46 | } 47 | 48 | resolve(files.filter((f) => nameMatch.test(f))) 49 | }) 50 | }) 51 | } 52 | 53 | /** 54 | * Generates responsive images for a list of files, 55 | * according to specified configuration and options 56 | * 57 | * @param {Array} files 58 | * @param {RegExp} pattern - Regexp to replace with 59 | * @returns {Promise} 60 | */ 61 | function renameImagesToSize (files, pattern) { 62 | const promises = [] 63 | 64 | files.forEach((file) => { 65 | promises.push(renameImageToSize(file, pattern)) 66 | }) 67 | 68 | return Promise.all(promises) 69 | } 70 | 71 | /** 72 | * Renames an image according to it's own width. 73 | * Example input: myimage@4x.png 74 | * Example output: myimage-1246x.png 75 | * 76 | * @param {String} input - Path to the image 77 | * @param {RegExp} pattern - Regexp to replace with 78 | * @returns {Promise} 79 | */ 80 | function renameImageToSize (input, pattern) { 81 | return new Promise((resolve, reject) => { 82 | const imagePath = path.parse(input) 83 | const image = sharp(input) 84 | 85 | if (!image) { 86 | return reject('Unable to open image file') 87 | } 88 | 89 | if (!pattern) { 90 | return reject('Pattern missing') 91 | } 92 | 93 | image 94 | .metadata() 95 | .then((metadata) => { 96 | const newNamePortion = `-${metadata.width}x` 97 | const execResult = pattern.exec(imagePath.name) 98 | const renamePortion = execResult && execResult.length > 0 ? execResult[1] : null 99 | 100 | if (!renamePortion) { 101 | debug((`Pattern did not match file ${input}, not doing anything for this file`)) 102 | resolve() 103 | } 104 | 105 | const newName = imagePath.name.replace(renamePortion, newNamePortion) 106 | const newPath = Object.assign({}, imagePath, {name: newName, base: `${newName}${imagePath.ext}`}) 107 | 108 | fs.rename(path.format(imagePath), path.format(newPath), (err) => { 109 | if (err) { 110 | reject(err) 111 | } else { 112 | resolve(path.format(newPath)) 113 | } 114 | }) 115 | }) 116 | }) 117 | } 118 | 119 | module.exports = { 120 | getResponsiveImages: getResponsiveImages, 121 | renameImageToSize: renameImageToSize, 122 | renameImagesToSize: renameImagesToSize, 123 | generateResponsiveImages: generateResponsiveImages 124 | } 125 | -------------------------------------------------------------------------------- /lib/size.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (neededSize, originalSize) { 4 | if (neededSize === undefined || neededSize === null) { 5 | return null 6 | } else if (typeof neededSize === 'string' && neededSize.indexOf('%') > -1) { 7 | const percentage = parseFloat(neededSize) 8 | 9 | if (isNaN(percentage)) { 10 | throw new Error(`Wrong percentage size "${neededSize}"`) 11 | } 12 | 13 | return Math.round(originalSize * percentage * 0.01) 14 | } else { 15 | neededSize = parseInt(neededSize) 16 | 17 | if (isNaN(neededSize)) { 18 | throw new Error(`Wrong size "${neededSize}"`) 19 | } 20 | 21 | return neededSize 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsive-images-generator", 3 | "version": "0.2.2", 4 | "description": "Generate responsive images", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard", 8 | "unit": "mocha", 9 | "fix": "standard --fix", 10 | "test": "npm run lint && npm run unit" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/felixrieseberg/responsive-images-generator.git" 15 | }, 16 | "keywords": [ 17 | "Responsive" 18 | ], 19 | "author": "Felix Rieseberg ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/felixrieseberg/responsive-images-generator/issues" 23 | }, 24 | "homepage": "https://github.com/felixrieseberg/responsive-images-generator#readme", 25 | "dependencies": { 26 | "debug": "^2.3.3", 27 | "rename": "^1.0.3", 28 | "sharp": "^0.16.2" 29 | }, 30 | "devDependencies": { 31 | "mocha": "^3.2.0", 32 | "standard": "^8.6.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/images/aileen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/responsive-images-generator/42143b81bdee6863978f1fb4410d187c08c293b1/test/images/aileen.jpg -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* globals describe, it, after */ 2 | const assert = require('assert') 3 | const path = require('path') 4 | const fs = require('fs') 5 | const methods = require('../lib') 6 | 7 | const getResponsiveImages = methods.getResponsiveImages 8 | const generateResponsiveImages = methods.generateResponsiveImages 9 | const renameImagesToSize = methods.renameImagesToSize 10 | 11 | const imgPath = path.join(__dirname, '../test/images/') 12 | 13 | describe('generateResponsiveImage', function () { 14 | after(() => { 15 | const files = fs.readdirSync(imgPath) 16 | .filter((f) => f !== 'aileen.jpg') 17 | 18 | files.forEach((f) => fs.unlinkSync(path.join(imgPath, f))) 19 | }) 20 | 21 | it('should generate some images', function () { 22 | this.timeout(5000) 23 | const configs = [ 24 | {width: '20%', rename: {suffix: '@1x'}}, 25 | {width: '40%', rename: {suffix: '@2x'}}, 26 | {width: '60%', rename: {suffix: '@3x'}}, 27 | {width: '80%', rename: {suffix: '@4x'}}, 28 | {width: '100%', rename: {suffix: '@5x'}} 29 | ] 30 | 31 | return generateResponsiveImages([path.join(imgPath, 'aileen.jpg')], configs) 32 | .then(() => { 33 | const contents = fs.readdirSync(path.join(__dirname, '../test/images/')).filter((f) => f !== '.DS_Store') 34 | const expected = ['aileen.jpg', 'aileen@1x.jpg', 'aileen@2x.jpg', 'aileen@3x.jpg', 'aileen@4x.jpg', 'aileen@5x.jpg'] 35 | 36 | assert.deepEqual(contents, expected) 37 | }) 38 | }) 39 | 40 | it('should resolve with a correct set of images', function () { 41 | return getResponsiveImages(imgPath) 42 | .then((r) => { 43 | assert.deepEqual(r, ['aileen@1x.jpg', 'aileen@2x.jpg', 'aileen@3x.jpg', 'aileen@4x.jpg', 'aileen@5x.jpg']) 44 | }) 45 | }) 46 | 47 | it('should rename some images', function () { 48 | this.timeout(8000) 49 | const pattern = /(?:.*)(@[0-9]{0,10}x)$/ 50 | const files = fs.readdirSync(imgPath) 51 | .filter((f) => f !== '.DS_Store') 52 | .map((f) => `${imgPath}${f}`) 53 | 54 | return renameImagesToSize(files, pattern) 55 | .then(() => { 56 | const actual = fs.readdirSync(imgPath) 57 | .filter((f) => f !== '.DS_Store') 58 | const expected = ['aileen-120x.jpg', 'aileen-180x.jpg', 'aileen-240x.jpg', 'aileen-300x.jpg', 'aileen-60x.jpg', 'aileen.jpg'] 59 | 60 | assert.deepEqual(actual, expected) 61 | }) 62 | }) 63 | }) 64 | --------------------------------------------------------------------------------