├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.js ├── lib └── crops.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .env 39 | tmp/ 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gabriel Nunes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crops 2 | Simple thumbnail generation server 3 | 4 | ### Clone & install dependencies 5 | ```sh 6 | sudo apt-get install imagemagick graphicsmagick 7 | git clone git@github.com:gnuns/crops.git 8 | cd crops/ 9 | npm install 10 | ``` 11 | 12 | ### Usage: 13 | Set the .env variables: 14 | ``` 15 | PORT=1337 16 | BASE_SERVER=http://cdn.example.com/ 17 | ``` 18 | 19 | Run! 20 | ``` 21 | npm start 22 | ``` 23 | 24 | To crop the image: 25 | `http://cdn.example.com/blah/awsome/potatoe.jpg` 26 | 27 | You just have to access 28 | `http://localhost:1337/crop/200/400/blah/awsome/potatoe.jpg` 29 | 30 | 31 | You can also set the crop gravity: 32 | `http://localhost:1337/crop/200/400/blah/awsome/potatoe.jpg?gravity=North` or 33 | `http://localhost:1337/crop/200/400/blah/awsome/potatoe.jpg?gravity=2` 34 | 35 | And quality (1-100): 36 | `http://localhost:1337/crop/200/400/blah/awsome/potatoe.jpg?quality=70` 37 | 38 | By default, Crops use quality = 100 and [smartcrop](https://github.com/jwagner/smartcrop.js) to define the best gravity 39 | 40 | 41 | #### Valid gravity values: 42 | | Code | Name | 43 | | --- | --- | 44 | | 0 | Center | 45 | | 1 | NorthWest | 46 | | 2 | North | 47 | | 3 | NorthEast | 48 | | 4 | West | 49 | | 5 | East | 50 | | 6 | SouthWest | 51 | | 7 | South | 52 | | 8 | SouthEast | 53 | | 9 | **Smart** (default) | 54 | 55 | 56 | ### Live demo 57 | 58 | I setup a live demo on heroku with `BASE_SERVER` param as `http://`, so you can view a cropped version of any image on the web using the URL without http:// 59 | 60 | Address: `https://crops.herokuapp.com/crop/` 61 | 62 | #### Example: 63 | 64 | ![Original cat](http://68.media.tumblr.com/4e097c1aba3644c09121b28c3fc2d468/tumblr_mgtkitzs2I1qlp8dho1_1280.jpg) 65 | 66 | `Image: http://68.media.tumblr.com/4e097c1aba3644c09121b28c3fc2d468/tumblr_mgtkitzs2I1qlp8dho1_1280.jpg` 67 | 68 | ![Square cat](https://crops.herokuapp.com/crop/300/300/68.media.tumblr.com/4e097c1aba3644c09121b28c3fc2d468/tumblr_mgtkitzs2I1qlp8dho1_1280.jpg?quality=70) 69 | 70 | `70% quality 300x300 image: https://crops.herokuapp.com/crop/300/300/68.media.tumblr.com/4e097c1aba3644c09121b28c3fc2d468/tumblr_mgtkitzs2I1qlp8dho1_1280.jpg?quality=70` 71 | 72 | ## credits 73 | inspired by [zooniverse/static-crop](https://github.com/zooniverse/static-crop) 74 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const express = require('express')() 3 | const crops = require('./lib/crops') 4 | const port = process.env.PORT || 8080 5 | 6 | 7 | express.get('/crop/:w/:h/*', crops.crop) 8 | express.listen(port, () => console.log(`Crops listening on port ${port}`)) 9 | -------------------------------------------------------------------------------- /lib/crops.js: -------------------------------------------------------------------------------- 1 | const got = require('got') 2 | const gm = require('gm').subClass({imageMagick: true}) 3 | const fs = require('fs') 4 | const smartcrop = require('smartcrop-gm') 5 | 6 | const MAX_CROP_WIDTH = 2000 7 | const MAX_CROP_HEIGHT = 2000 8 | 9 | const gravityMap = { 10 | 0: 'Center', 11 | 1: 'NorthWest', 12 | 2: 'North', 13 | 3: 'NorthEast', 14 | 4: 'West', 15 | 5: 'East', 16 | 6: 'SouthWest', 17 | 7: 'South', 18 | 8: 'SouthEast', 19 | 9: 'Smart' 20 | } 21 | 22 | module.exports = { crop } 23 | 24 | function crop (req, res, smart) { 25 | res.set('X-Powered-By', 'Crops') 26 | 27 | let params = req.params 28 | 29 | params.gravity = req.query.gravity || 'Smart' 30 | params.quality = req.query.quality || 100 31 | 32 | 33 | // int gravity to the matching string value 34 | if (!isNaN(parseInt(params.gravity))) params.gravity = gravityMap[params.gravity] 35 | if (isNaN(parseInt(params.quality))) params.quality = 100 36 | if (params.w > MAX_CROP_WIDTH || params.h > MAX_CROP_HEIGHT) { 37 | res 38 | .status(500) 39 | .send(`Error: max width: ${MAX_CROP_WIDTH}, max height: ${MAX_CROP_HEIGHT}`) 40 | } 41 | 42 | let img 43 | let cropFunction = params.gravity == 'Smart' ? smartCrop : simpleCrop 44 | 45 | downloadImage(params[0]) 46 | .then((_img) => { 47 | img = _img 48 | return cropFunction(params, img.path) 49 | }) 50 | .then((_gm) => { 51 | _gm 52 | .quality(params.quality) 53 | .stream((err, stdout, stderr) => { 54 | if (err) return res.status(500).send(err) 55 | try { 56 | setTimeout(() => fs.unlink(img.path), 1500) 57 | res.set('Content-Type', img.type) 58 | stdout.pipe(res) 59 | } catch (e) { 60 | res.status(500).send(`Error: ${err}`) 61 | } 62 | }) 63 | }) 64 | .catch((err) => res.status(500).send(`Error: ${err}`)) 65 | } 66 | 67 | function smartCrop (params, imgPath) { 68 | return new Promise((resolve, reject) => { 69 | let img = fs.readFileSync(imgPath) 70 | smartcrop 71 | .crop(img, {width: params.w, height: params.h}) 72 | .then(function({topCrop}) { 73 | let _gm = gm(imgPath) 74 | _gm 75 | .crop(topCrop.width, topCrop.height, topCrop.x, topCrop.y) 76 | .resize(params.w, params.h) 77 | return resolve(_gm) 78 | }) 79 | .catch(reject) 80 | }) 81 | } 82 | 83 | function simpleCrop (params, imgPath) { 84 | return new Promise((resolve, reject) => { 85 | let _gm = gm(imgPath) 86 | _gm 87 | .resize(params.w, params.h, '^') 88 | .gravity(params.gravity) 89 | .crop(params.w, params.h) 90 | return resolve(_gm) 91 | }) 92 | } 93 | 94 | function mountURL (url, path) { 95 | url += `${(url.endsWith('/') ? '' : '/')}${path}` 96 | return url 97 | } 98 | 99 | function writeFile (file) { 100 | if (!fs.existsSync('tmp/')) fs.mkdirSync('tmp/') 101 | if (!file.type.startsWith('image')) throw 'Wrong file type!' 102 | let path = `tmp/crops-${Math.random()}.${file.type.split('/').pop()}` 103 | fs.writeFileSync(path, file.content, 'binary') 104 | return path 105 | } 106 | 107 | async function requestFile (url) { 108 | let response = await got(url, {encoding: 'binary'}) 109 | if (!response.body || response.statusCode !== 200) throw 'Request failed!' 110 | return {type: response.headers['content-type'], content: response.body} 111 | } 112 | 113 | async function downloadImage (location) { 114 | let url = mountURL(process.env.BASE_SERVER, location) 115 | let file = await requestFile(url) 116 | let path = writeFile(file) 117 | return {path, type: file.type} 118 | } 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crops", 3 | "version": "1.2.0", 4 | "description": "image cronp and resize server", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/gnuns/crops.git" 13 | }, 14 | "author": "Gabriel Nunes", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/gnuns/crops/issues" 18 | }, 19 | "homepage": "https://github.com/gnuns/crops#readme", 20 | "dependencies": { 21 | "dotenv": "^4.0.0", 22 | "express": "^4.15.0", 23 | "gm": "^1.23.0", 24 | "got": "^7.1.0", 25 | "smartcrop-gm": "^1.0.2" 26 | } 27 | } 28 | --------------------------------------------------------------------------------