├── Procfile
├── .gitignore
├── package.json
├── src
├── mimetypes.js
├── settings.js
├── tempfile.js
├── main.js
├── util.js
├── UrlParser.js
└── Server.js
├── static
├── img2x.html
├── utils.js
└── quality.html
├── test
└── UrlParserTest.js
└── README.markdown
/Procfile:
--------------------------------------------------------------------------------
1 | web: node src/main.js $PORT
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | tmp/
2 | img
3 | *.local.js
4 | *.log
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodejs-resize-image",
3 | "version": "0.0.1",
4 | "description": "wanna resize",
5 | "repository": {
6 | "type": "git"
7 | },
8 | "scripts": {
9 | "start": "node src/main.js"
10 | },
11 | "engines": {
12 | "node": "0.10.x"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/mimetypes.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | var EXTENSIONS = {
4 | '.jpg': 'image/jpeg',
5 | '.jpeg': 'image/jpeg',
6 | '.png': 'image/png',
7 | '.gif': 'image/gif',
8 | '.tif': 'image/tiff',
9 | '.html': 'text/html',
10 | '.htm': 'text/html',
11 | '.js': 'text/javascript',
12 | '.json': 'application/json'
13 | };
14 |
15 | function getMimeType(name) {
16 | var ext = path.extname(name).toLowerCase();
17 | return EXTENSIONS[ext] || 'application/octet-stream';
18 | };
19 |
20 | exports.fromPath = getMimeType;
21 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | var config = {};
2 | config.port = 8383;
3 | config.host = '0.0.0.0';
4 |
5 | //imagemagick command. can be absolute path: /usr/bin/convert
6 | config.convertCmd = 'convert';
7 |
8 | //where images are located. can be absolute path.
9 | config.srcDir = './img/';
10 | //where to store resized files.
11 | config.destDir = './tmp/';
12 | //whether to leave generated thumbnails or not.
13 | config.cacheImages = true;
14 |
15 | // where static files are located.
16 | config.staticDir = './static/';
17 |
18 | //maximum supported output image size.
19 | config.maxOutputSize = 2000;
20 |
21 | config.debug = false;
22 |
23 | var localConfig = {};
24 | try {
25 | localConfig = require('./settings.local');
26 | } catch(err) {
27 | } finally {
28 | localConfig.__proto__ = config;
29 | }
30 |
31 | module.exports = localConfig;
32 |
--------------------------------------------------------------------------------
/src/tempfile.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var os = require('os');
3 |
4 | var util = require('./util');
5 |
6 | var defaultRootDir = os.tmpdir();
7 | var defaultPrefix = "tmp-";
8 | var defaultPostfix = "";
9 |
10 | var generateName = function() {
11 | return '%s-%s'.f(
12 | Date.now()
13 | , (Math.random() * 0x100000000 + 1).toString(36));
14 | };
15 |
16 |
17 |
18 | var getName = function(prefix, postfix) {
19 | if (undefined === prefix) {
20 | prefix = defaultPrefix;
21 | }
22 | if (undefined === postfix) {
23 | postfix = defaultPostfix;
24 | }
25 | return prefix + generateName() + postfix;
26 | };
27 |
28 | var getPath = function(prefix, postfix, rootDir) {
29 | if (undefined === rootDir) {
30 | rootDir = defaultRootDir;
31 | }
32 | return path.join(rootDir, getName(prefix, postfix));
33 | };
34 |
35 | exports.getPath = getPath;
36 | exports.getName = getName;
37 |
--------------------------------------------------------------------------------
/static/img2x.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Image Quality Test
4 |
9 |
10 |
11 |
12 |
49 |
50 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | var util = require('./util');
4 | var settings = require('./settings');
5 | var Server = require('./Server');
6 |
7 | var main = function() {
8 | var argv = process.argv.splice(2);
9 | var port = argv.length > 0 ? (argv[0] * 1) : settings.port;
10 | var host = settings.host;
11 | var convertCmd = settings.convertCmd;
12 | var srcDir = settings.srcDir;
13 | var destDir = settings.destDir;
14 | var staticDir = settings.staticDir;
15 | var baseDir = process.cwd();//__dirname;
16 | var cacheImages = settings.cacheImages;
17 | var maxOutputSize = settings.maxOutputSize;
18 | var debug = settings.debug;
19 |
20 | if (!srcDir.startsWith('/')) {
21 | srcDir = path.join(baseDir, srcDir);
22 | }
23 | if (!destDir.startsWith('/')) {
24 | destDir = path.join(baseDir, destDir);
25 | }
26 | if (!staticDir.startsWith('/')) {
27 | staticDir = path.join(baseDir, staticDir);
28 | }
29 |
30 |
31 |
32 | console.log('Using\n'
33 | + ' host %s\n'.f(host)
34 | + ' port %s\n'.f(port)
35 | + ' convert cmd %s\n'.f(convertCmd)
36 | + ' staticDir %s\n'.f(staticDir)
37 | + ' srcDir %s\n'.f(srcDir)
38 | + ' destDir %s\n'.f(destDir)
39 | + ' cacheImages %s\n'.f(cacheImages)
40 | + ' maxOutputSize %s\n'.f(maxOutputSize)
41 | + ' debug %s\n'.f(debug)
42 | );
43 |
44 | var server = Server(convertCmd, srcDir, destDir, cacheImages, maxOutputSize, staticDir);
45 | var exit = function(ret) {
46 | if (typeof ret === 'undefined') {
47 | ret = 0;
48 | }
49 | server.stop();
50 | console.log('Bye');
51 | process.exit(ret);
52 | };
53 |
54 | //process.on('exit', exit);
55 | process.on('SIGINT', exit);
56 | process.on('SIGTERM', exit);
57 | process.on('SIGQUIT', exit);
58 | //process.on('uncaughtException', function(err) {
59 | // console.log(err);
60 | // console.log(err.stack);
61 | // //exit(1);
62 | //});
63 | server.start(host, port);
64 | };
65 |
66 | main();
67 |
--------------------------------------------------------------------------------
/test/UrlParserTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 |
3 | var UrlParser = require('../src/UrlParser');
4 |
5 | describe('UrlParser', function() {
6 | describe('#parse()', function() {
7 | var urlParser = UrlParser('/src', '/out');
8 |
9 | var centerCropRemote = urlParser.parse('/http://imgur.com/a.100x200.jpg');
10 |
11 | it('center crop remote -> remoteUrl', function() {
12 | assert.equal('http://imgur.com/a.jpg', centerCropRemote.remoteUrl);
13 | });
14 |
15 | it('center crop remote -> out', function() {
16 | assert.equal('/out/http/imgur.com/a.100x200.jpg', centerCropRemote.out);
17 | });
18 |
19 | it('center crop remote -> src', function() {
20 | assert.equal('/src/http/imgur.com/a.jpg', centerCropRemote.src);
21 | });
22 |
23 | var centerCropRemoteNormalized = urlParser.parse('/http/imgur.com/a.100x200.jpg');
24 |
25 | it('center crop remote normalized -> remoteUrl', function() {
26 | assert.equal('http://imgur.com/a.jpg', centerCropRemoteNormalized.remoteUrl);
27 | });
28 |
29 | it('center crop remote normalized -> out', function() {
30 | assert.equal('/out/http/imgur.com/a.100x200.jpg', centerCropRemoteNormalized.out);
31 | });
32 |
33 | it('center crop remote normalized -> src', function() {
34 | assert.equal('/src/http/imgur.com/a.jpg', centerCropRemoteNormalized.src);
35 | });
36 |
37 | var badProtocol = urlParser.parse('/httpa/imgur.com/a.100x200sw.jpg');
38 | it('httpa -> remoteUrl', function() {
39 | assert.equal(null, badProtocol.remoteUrl);
40 | });
41 |
42 | it('httpa -> src', function() {
43 | assert.equal('/src/httpa/imgur.com/a.jpg', badProtocol.src);
44 | });
45 |
46 |
47 | it('httpa -> out', function() {
48 | assert.equal('/out/httpa/imgur.com/a.100x200sw.jpg', badProtocol.out);
49 | });
50 |
51 | it('a', function() {
52 | assert.equal(null, urlParser.parse('/http:/imgur.com/a.100x200t.jpg').remoteUrl);
53 | });
54 |
55 | it('b', function() {
56 | assert.equal('http://imgur.com/a.jpg', urlParser.parse('/http/imgur.com/a.100x200sw.jpg').remoteUrl);
57 | });
58 |
59 | it('c', function() {
60 | assert.equal('https://imgur.com/a.jpg', urlParser.parse('/https/imgur.com/a.100x200sw.jpg').remoteUrl);
61 | });
62 |
63 | it('d', function() {
64 | assert.equal('https://imgur.com/a.100x200abc.jpg', urlParser.parse('/https/imgur.com/a.100x200abc.jpg').remoteUrl);
65 | });
66 |
67 | it('e', function() {
68 | assert.equal('https://imgur.com/a.jpg', urlParser.parse('/https://imgur.com/a.100x200sw.jpg').remoteUrl);
69 | });
70 |
71 | it('dirname a', function() {
72 | assert.equal('https://imgur.com/a.jpg', urlParser.parse('/https://imgur.com/a.100x200sw.jpg').remoteUrl);
73 | });
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | if (typeof Object.create !== 'function') {
2 | Object.create = function(o) {
3 | var F = function() {};
4 | F.prototype = o;
5 | return new F();
6 | };
7 | }
8 |
9 | if (String.prototype.f !== 'function') {
10 | String.prototype.f = function() {
11 | var args = arguments;
12 | var index = 0;
13 | var r = function() {
14 | return args[index++];
15 | };
16 | return this.replace(/%s/g, r);
17 | };
18 | }
19 |
20 | if (String.prototype.fo !== 'function') {
21 | String.prototype.fo = function(o) {
22 | var r = function(matched, group_1) {
23 | return o[group_1];
24 | };
25 | return this.replace(/%\(([^)]+)\)s/g, r);
26 | };
27 | }
28 |
29 | if (String.prototype.startsWith !== 'function') {
30 | String.prototype.startsWith = function(s) {
31 | return this.indexOf(s) === 0;
32 | };
33 | }
34 |
35 | /*
36 | if (Array.prototype.collectFirst !== 'function') {
37 | Array.prototype.collectFirst = function(pred) {
38 | var n = this.length;
39 | var i = 0;
40 | for (; i < n; i++) {
41 | var x = this[i];
42 | if (pred(x)) {
43 | return x;
44 | }
45 | }
46 | return null;
47 | };
48 | }
49 | */
50 |
51 | var path = require('path');
52 | var fs = require('fs');
53 | var http = require('http');
54 | var https = require('https');
55 |
56 | var mkdirP = function(p, mode, callback) {
57 | var cb = callback || function () {};
58 | if (p.charAt(0) !== '/') {
59 | p = path.join(process.cwd(), p);
60 | }
61 |
62 | var ps = path.normalize(p).split('/');
63 | fs.exists(p, function (exists) {
64 | if (exists) {
65 | cb(null);
66 | } else {
67 | mkdirP(ps.slice(0,-1).join('/'), mode, function (err) {
68 | if (err && err.errno != process.EEXIST) {
69 | cb(err);
70 | } else {
71 | fs.mkdir(p, mode, cb);
72 | }
73 | });
74 | }
75 | });
76 | };
77 |
78 |
79 | var downloadAnd = function(url, target, callback) {
80 | var GET = http.get;
81 | if (url.startsWith('https://')) {
82 | GET = https.get;
83 | }
84 | console.log('downloading %s => %s', url, target);
85 | GET(url, function(resp) {
86 | if (resp.statusCode !== 200) {
87 | callback(new Error("remote server didn't respond with 200: "+url));
88 | return;
89 | }
90 |
91 | //maybe download to /tmp and rename.
92 | var file = fs.createWriteStream(target);
93 | resp.on('data', function(chunk) {
94 | file.write(chunk);
95 | }).on('end', function() {
96 | file.end();
97 | callback(null);
98 | });
99 | }).on('error', function(err) {
100 | callback(err);
101 | }).setTimeout(5000);
102 | };
103 |
104 |
105 | /**
106 | * downloads url and calls callback(error or null);
107 | */
108 | var ensureDirAndDownload = function(url, target, callback) {
109 | var targetDir = path.dirname(target);
110 | fs.exists(targetDir, function(exists) {
111 | if (!exists) {
112 | mkdirP(targetDir, 0755, function(err) {
113 | if (err) {
114 | callback(err);
115 | } else {
116 | downloadAnd(url, target, callback);
117 | }
118 | });
119 | } else {
120 | downloadAnd(url, target, callback);
121 | }
122 | });
123 | };
124 |
125 | module.exports = {
126 | mkdirP: mkdirP,
127 | downloadAnd: ensureDirAndDownload
128 | };
129 |
130 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | # About
2 |
3 | For GET requests, dynamically crops and resizes image using
4 | ImageMagick `convert` command.
5 | And, serves the resized image.
6 |
7 |
8 | The following crops 1:2 rectangle at center of some/image.jpg. And resizes the crop to 10x20:
9 |
10 | ```
11 | GET /some/image.10x20.jpg
12 | ```
13 |
14 | You can specify where to crop other than center. The following crops at the top (north).
15 |
16 | ```
17 | GET /some/image.10x20n.jpg
18 | ```
19 |
20 | You can specify crop size in percentage (defaults to `100%`):
21 |
22 | ```
23 | GET /some/image.10x20n.70.jpg
24 | ```
25 |
26 | would crop 1:2 rectangle that'll fit about 70% of original image.
27 |
28 | Other parameters:
29 |
30 | Parameter | Crops At | Example | Comment
31 | ----------|----------|---------|---------
32 | | Center | GET /some/image.10x20.jpg | Not specifying a parameter crops at center.
33 | n | North | GET /a/b.100x400n.jpg |
34 | s | South | GET /a/b.100x400s.jpg |
35 | w | West | GET /a/b.100x400w.jpg |
36 | e | East | GET /a/b.100x400e.jpg |
37 | nw | NorthWest | GET /a/b.100x400nw.jpg |
38 | ne | NorthEast | GET /a/b.100x400ne.jpg |
39 | sw | SouthWest | GET /a/b.100x400sw.jpg |
40 | se | SouthEast | GET /a/b.100x400se.jpg |
41 | t | No Crop | GET /some/image.100x400t.jpg | Instead of cropping, some/image.jpg is resized to fit in 100x400 rectangle.
42 |
43 | # Example
44 |
45 | - [original is horizontal.](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.jpg)
46 | - Horizontal to square:
47 | - [300x300 cropped to center](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.300x300.jpg)
48 | - [300x300 cropped to west](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.300x300w.jpg)
49 | - [300x300 cropped to east](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.300x300e.jpg)
50 | - [300x300 cropped to north west 100%](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.300x300nw.jpg)
51 | - [300x300 cropped to north west 50%](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.300x300nw.50.jpg)
52 | - [300x300 thumbnail](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.300x300t.jpg)
53 | - Horizontal to vertical:
54 | - [320x480 cropped to center](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.320x480.jpg)
55 | - [320x480 cropped to west](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.320x480w.jpg)
56 | - [320x480 cropped to east](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.320x480e.jpg)
57 | - [320x480 cropped to north east 100%](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.320x480ne.jpg)
58 | - [320x480 cropped to north east 75%](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.320x480ne.75.jpg)
59 | - Horizontal to horizontal:
60 | - [500x281 cropped to center](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.500x281.jpg)
61 | - [500x281 cropped to north](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.500x281n.jpg)
62 | - [500x281 cropped to south](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.500x281s.jpg)
63 | - [500x281 cropped to south west 100%](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.500x281sw.jpg)
64 | - [500x281 cropped to south west 80%](http://nodejs-resize-image.herokuapp.com/http://i.imgur.com/gWthS3m.500x281sw.80.jpg)
65 |
66 | # Quickstart
67 |
68 | Run
69 |
70 | node src/main.js
71 |
72 | Configure
73 |
74 | cp src/settings.js src/settings.local.js
75 | vim settings.local.js
76 |
77 | Example `settings.local.js`:
78 |
79 |
80 | module.exports = {
81 | port: 8081,
82 | srcDir: '/var/www',
83 | destDir: '/var/cache',
84 | convertcmd: '/usr/local/bin/convert'
85 | }
86 |
--------------------------------------------------------------------------------
/static/utils.js:
--------------------------------------------------------------------------------
1 | function parseQueryString(query) {
2 | query = decodeURIComponent(query || window.location.search.substring(1));
3 |
4 | var args = {};
5 | query.split(/&/).forEach(function(kv) {
6 | var splitted = kv.split(/=/);
7 | args[splitted[0]] = splitted[1];
8 | });
9 | return args;
10 | }
11 |
12 | function parseAssetUrl(url) {
13 | var regex = new RegExp('^(.+)(\\.\\w+)$');
14 | var m = regex.exec(url);
15 | return m;
16 | }
17 |
18 | function getRendition(url, width, height, isNoCrop, quality) {
19 | var parsed = parseAssetUrl(url);
20 | if (!parsed) {
21 | return null;
22 | }
23 |
24 |
25 | var withoutExt = parsed[1];
26 | var ext = parsed[2];
27 |
28 | var args = [];
29 | if (!isNaN(width) && !isNaN(height)) {
30 | var dimension = width+'x'+height;
31 | if (isNoCrop) {
32 | dimension += 't';
33 | }
34 | args.push(dimension);
35 | }
36 |
37 | if (!isNaN(quality)) {
38 | args.push('q'+quality);
39 | }
40 |
41 | return '/'+withoutExt+'.'+args.join('.')+ext;
42 | }
43 |
44 | function createAnchor(url, text) {
45 | var a = document.createElement('a');
46 | a.href = url;
47 | a.innerHTML = text || url;
48 | return a;
49 | }
50 |
51 | function range(start, end, stepsFn) {
52 | if (start === end) {
53 | return [start, end];
54 | }
55 |
56 | var shouldLoop = function() {
57 | return start < end;
58 | };
59 | if (start > end) {
60 | shouldLoop = function() {
61 | return start > end;
62 | };
63 | }
64 |
65 | if (typeof stepsFn !== 'function') {
66 | stepsFn = function(x) { return x + 1; };
67 | }
68 | var arr = [];
69 |
70 |
71 | while (shouldLoop()) {
72 | arr.push(start);
73 | start = stepsFn(start);
74 | }
75 | arr.push(end);
76 | return arr;
77 | }
78 |
79 | function initFormInputFromQuery(query, key, form, defaultValue, name) {
80 | name = name || key;
81 |
82 | var input = form.querySelector('input[name="'+name+'"]');
83 | var value = query[key];
84 | if (typeof value === 'undefined') {
85 | value = defaultValue;
86 | }
87 |
88 | if (input) {
89 | input.value = value;
90 | }
91 | return input;
92 | }
93 |
94 | function commaSeparatedNumbers(str) {
95 | if (str) {
96 | return str.split(',').map(function(x) { return Number(x); });
97 | }
98 | return [];
99 | }
100 |
101 | function decimalDigits(num) {
102 | return Math.max(Math.log(Math.floor(num)) / Math.LN10 + 1, 1);
103 | }
104 |
105 | function toFractionPrecision(num, precision) {
106 | return num.toPrecision(decimalDigits(num) + precision);
107 | }
108 |
109 | function toKilobyte(size) {
110 | return toFractionPrecision(size / Math.pow(2, 10), 2);
111 | }
112 |
113 | // loads image via xhr to get image byte size :P
114 | // onSuccess(Image, int byte size)
115 | function loadImage(url, onSuccess) {
116 | var xhr = new XMLHttpRequest();
117 | xhr.responseType = 'blob';
118 |
119 | xhr.addEventListener('load', function() {
120 | var blob = xhr.response;
121 |
122 | var reader = new FileReader();
123 | reader.addEventListener('loadend', function() {
124 | var blobBase64 = reader.result;
125 | var img = new Image();
126 | img.src = blobBase64;
127 | if (typeof onSuccess == 'function') {
128 | var size = parseInt(xhr.getResponseHeader('Content-Length'), 10) || blob.size;
129 | onSuccess(img, size);
130 | }
131 | });
132 |
133 | reader.readAsDataURL(xhr.response);
134 | });
135 | xhr.open('GET', url);
136 | xhr.send();
137 | }
138 |
139 | function appendImageCell(cellWidth, cellHeight, quality, url, parentElem) {
140 | var cell = document.createElement('div');
141 | cell.className = 'cell';
142 | cell.style.width = cellWidth + 'px';
143 |
144 | function append(img, size) {
145 |
146 | img.addEventListener('load', function() {
147 | var kb = toKilobyte(size);
148 | cell.appendChild(document.createElement('br'));
149 |
150 | var msg = img.naturalWidth+'x'+img.naturalHeight+' | q'+quality+' | '+kb+'kb';
151 | cell.appendChild(document.createTextNode(msg));
152 | cell.appendChild(document.createElement('br'));
153 |
154 | cell.appendChild(createAnchor(url));
155 |
156 | });
157 |
158 |
159 | cell.appendChild(img);
160 | }
161 |
162 |
163 | parentElem.appendChild(cell);
164 | loadImage(url, append);
165 | }
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/src/UrlParser.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var util = require('./util');
3 |
4 | /**
5 | * require('UrlParser')(srcDir, destDir).parse(url);
6 | *
7 | * paramatized module
8 | */
9 | var Module = function(srcDir, destDir, maxOutputSize) {
10 | var me = {};
11 |
12 |
13 | var PathUrlRE = /^\/*(https?)(?:\:\/)?\/(.+)$/;
14 | var PathRE = /^\/*(.+)$/;
15 | var HttpUrlRE = /^(https?)(?:\:\/)?\/(.+)$/;
16 |
17 | var normalizeUrl = function(url) {
18 | var m = PathUrlRE.exec(url);
19 | if (!m) {
20 | var pathMatch = PathRE.exec(url);
21 | return pathMatch[1];
22 | }
23 | return path.join(m[1], m[2]);
24 | };
25 |
26 | var normalizeSize = function(width, height) {
27 | var w, h;
28 | if (width > height) {
29 | h = maxOutputSize * height / width;
30 | w = maxOutputSize;
31 | } else {
32 | w = maxOutputSize * width / height;
33 | h = maxOutputSize;
34 | }
35 | if (w >= width && h >= height) {
36 | w = width;
37 | h = height;
38 | }
39 | return {width: w, height: h};
40 | };
41 |
42 |
43 | var getImgPath = function(imgId) {
44 | return path.join(srcDir, normalizeUrl(imgId));
45 | };
46 |
47 | var getOutputPath = function(url) {
48 | return path.join(destDir, normalizeUrl(url));
49 | };
50 |
51 | var getRemoteOriginUrl = function(imgId) {
52 | var m = HttpUrlRE.exec(imgId);
53 | if (!m) {
54 | return null;
55 | }
56 | return m[1] + '://' + m[2];
57 | };
58 |
59 | function getQuality(str) {
60 | if (str && str.startsWith('.q')) {
61 | return parseInt(str.substring(2), 10);
62 | }
63 | return null;
64 | }
65 |
66 | function maybeAddQuality(args, quality) {
67 | if (quality) {
68 | args.push('-quality');
69 | args.push(quality);
70 | }
71 | }
72 |
73 | var CropParser = function() {
74 | var me = {};
75 |
76 | var UrlRE = /^\/(.+)\.(\d+)x(\d+)(n|w|s|e|nw|ne|sw|se)?(\.\d+)?(\.q\d+)?(\.\w+)$/;
77 | var GravityMap = {
78 | n: 'North',
79 | w: 'West',
80 | s: 'South',
81 | e: 'East',
82 | nw: 'NorthWest',
83 | ne: 'NorthEast',
84 | sw: 'SouthWest',
85 | se: 'SouthEast'
86 | };
87 |
88 | var getGravity = function(key) {
89 | if (!key) {
90 | return 'Center';
91 | }
92 | return GravityMap[key];
93 | };
94 |
95 | var MIN_CROP_PERCENT = 10;
96 | var MAX_CROP_PERCENT = 100;
97 |
98 | var getCropPercentage = function(percent) {
99 | if (typeof percent === 'undefined') {
100 | return undefined;
101 | }
102 | percent = percent.substring(1) * 1;//get rid of leading dot.
103 | if (Number.isNaN(percent)) {
104 | return undefined;
105 | }
106 | if (percent <= MIN_CROP_PERCENT) {
107 | return MIN_CROP_PERCENT;
108 | }
109 | if (percent >= MAX_CROP_PERCENT) {
110 | return MAX_CROP_PERCENT;
111 | }
112 | return percent;
113 | };
114 |
115 | me.parse = function(url) {
116 | var m = UrlRE.exec(url);
117 | if (!m) {
118 | return null;
119 | }
120 |
121 | var size = normalizeSize(m[2]*1, m[3]*1);
122 | var width = size.width;
123 | var height = size.height;
124 | var gravity = getGravity(m[4]);
125 | var cropPercentage = getCropPercentage(m[5]);
126 | var quality = getQuality(m[6]);
127 | var ext = m[7];
128 | var imgId = m[1] + ext;
129 |
130 | var src = getImgPath(imgId);
131 | var out = getOutputPath(url);
132 | var remoteUrl = getRemoteOriginUrl(imgId);
133 |
134 | var initialResizeWidth = width;
135 | var initialResizeHeight = height;
136 | if (cropPercentage) {
137 | var initialCropRatio = -1/100 * cropPercentage + 2;
138 | initialResizeWidth = Math.floor(width * initialCropRatio);
139 | initialResizeHeight = Math.floor(height * initialCropRatio);
140 | }
141 |
142 | var args = ['-coalesce', '-resize', '%sx%s^'.f(initialResizeWidth, initialResizeHeight),
143 | '-gravity', gravity,
144 | '-crop', '%sx%s+0+0'.f(width, height),
145 | '+repage'];
146 | maybeAddQuality(args, quality);
147 |
148 | return ({
149 | remoteUrl: remoteUrl,
150 | width: width,
151 | height: height,
152 | src: src,
153 | out: out,
154 | args: args
155 | });
156 | };
157 | return me;
158 | }
159 |
160 | var ThumbnailParser = function() {
161 | var me = {};
162 |
163 | var UrlRE = /^\/(.+)\.(\d+)x(\d+)t(\.q\d+)?(\.\w+)$/;
164 |
165 | me.parse = function(url) {
166 | var m = UrlRE.exec(url);
167 | if (!m) {
168 | return null;
169 | }
170 |
171 |
172 | var size = normalizeSize(m[2]*1, m[3]*1);
173 | var width = size.width;
174 | var height = size.height;
175 | var quality = getQuality(m[4]);
176 | var ext = m[5];
177 | var imgId = m[1] + ext;
178 |
179 | var src = getImgPath(imgId);
180 | var out = getOutputPath(url);
181 | var remoteUrl = getRemoteOriginUrl(imgId);
182 |
183 | var args = ['-coalesce', '-resize', '%sx%s>'.f(width, height)];
184 | maybeAddQuality(args, quality);
185 |
186 | return ({
187 | remoteUrl: remoteUrl,
188 | width: width,
189 | height: height,
190 | src: src,
191 | out: out,
192 | args: args
193 | });
194 | };
195 | return me;
196 | };
197 |
198 | var OriginalImgParser = function() {
199 | var me = {};
200 | var UrlRE = /^\/(.+\.\w+)$/;
201 | me.parse = function(url) {
202 | var m = UrlRE.exec(url);
203 | if (!m) {
204 | return null;
205 | }
206 | var imgId = m[1];
207 | var src = getImgPath(imgId);
208 | var remoteUrl = getRemoteOriginUrl(imgId);
209 | return ({
210 | remoteUrl: remoteUrl,
211 | src: src
212 | });
213 | };
214 | return me;
215 | };
216 |
217 |
218 | var parsers = [CropParser(), ThumbnailParser(), OriginalImgParser()];
219 | var parsersLength = parsers.length;
220 |
221 | me.parse = function(url) {
222 | var i = 0;
223 | for (; i < parsersLength; i++) {
224 | var parser = parsers[i];
225 | var parsed = parser.parse(url);
226 | if (parsed) {
227 | return parsed;
228 | }
229 | }
230 | return null;
231 | };
232 |
233 | return me;
234 | };
235 |
236 | module.exports = Module;
237 |
238 |
--------------------------------------------------------------------------------
/static/quality.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Image Quality Curve
4 |
12 | 0,0
13 |
14 |
27 |
28 |
29 |
295 |
--------------------------------------------------------------------------------
/src/Server.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var fs = require('fs');
3 | var path = require('path');
4 | var child = require('child_process');
5 | var events = require('events');
6 | var urlutil = require('url');
7 |
8 | var util = require('./util');
9 | var UrlParser = require('./UrlParser');
10 | var settings = require('./settings');
11 | var mimetypes = require('./mimetypes');
12 |
13 | var Server = function(convertCmd, srcDir, destDir, cacheImages, maxOutputSize, staticDir) {
14 | var me = {};
15 |
16 | var urlParser = UrlParser(srcDir, destDir, maxOutputSize);
17 |
18 |
19 | //handles request.
20 | var Handler = function(request, response) {
21 | var me = {};
22 |
23 | var emitter = new events.EventEmitter();
24 | me.emitter = emitter;
25 |
26 | var serveFile = function(filePath, mimeType, callback) {
27 | if (!mimeType) {
28 | mimeType = mimetypes.fromPath(filePath);
29 | }
30 |
31 | var encoding = 'binary';
32 |
33 | fs.stat(filePath, function(err, stat) {
34 | if (err || !stat.isFile()) {
35 | emitter.emit('error', me.do404, 'not found: ' + filePath);
36 | return;
37 | }
38 |
39 | var ifModifiedSince = new Date(request.headers['if-modified-since']);
40 | if (!isNaN(ifModifiedSince.getTime()) && ifModifiedSince >= stat.mtime) {
41 | response.writeHead(304);
42 | response.end();
43 | return;
44 | }
45 |
46 | response.writeHead(200, {
47 | 'Content-Type': mimeType,
48 | 'Content-Length': stat.size,
49 | 'Last-Modified': stat.mtime
50 | });
51 | var stream = fs.createReadStream(filePath, {
52 | flags: 'r'
53 | , encoding: encoding
54 | });
55 | stream.on('data', function(data) {
56 | response.write(data, encoding);
57 | });
58 | stream.on('close', function() {
59 | response.end();
60 | if (typeof callback === 'function') {
61 | callback();
62 | }
63 | });
64 | stream.on('error', function(err) {
65 | emitter.emit('error', me.do500, 'error reading file: ' + filePath);
66 | });
67 | });
68 | };
69 |
70 | var resizeImage = function(src, out, convertArgs, callback) {
71 | var basePath = path.dirname(out);
72 |
73 | var doResize = function() {
74 | var args = [src].concat(convertArgs);
75 | args.push(out);
76 | console.log('executing: %s %s', convertCmd, args.join(' '));
77 | var p = child.spawn(convertCmd, args);
78 | p.on('exit', function(code) {
79 | if (code === 0) {
80 | if (typeof callback === 'function') {
81 | callback(out);
82 | }
83 | } else {
84 | emitter.emit('error', me.do500, 'convert %s exited with %s'.f(args.join(' '), code));
85 | }
86 | });
87 | };
88 |
89 | util.mkdirP(basePath, 0755, function(err) {
90 | if (err) {
91 | emitter.emit('error', me.do500, 'cannot create directory: ' + basePath);
92 | } else {
93 | process.nextTick(doResize);
94 | }
95 | });
96 |
97 | };
98 |
99 | me.do404 = function(msg) {
100 | response.writeHead(404, {
101 | 'Content-Type': 'text/plain'
102 | });
103 | var data = 'Not Found: ' + request.url;
104 | if (msg) {
105 | data = '%s\n%s'.f(data, msg);
106 | }
107 | response.end(data);
108 | };
109 |
110 | me.do500 = function(msg) {
111 | response.writeHead(500, {
112 | 'Content-Type': 'text/plain'
113 | });
114 | var data = 'Internal Error: ' + request.url;
115 | if (msg) {
116 | data = '%s\n%s'.f(data, msg);
117 | }
118 | response.end(data);
119 | };
120 |
121 | me.do501 = function(msg) {
122 | response.writeHead(501, {
123 | 'Content-Type': 'text/plain'
124 | });
125 | var data = 'Not Supported Method: %s %s'.f(request.method, request.url);
126 | if (msg) {
127 | data = '%s\n%s'.f(data, msg);
128 | }
129 | response.end(data);
130 | };
131 |
132 |
133 | //serves cached image at imagePath (and deletes possibly).
134 | var serveCachedImage = function(imagePath) {
135 | serveFile(imagePath, undefined, function() {
136 | if (!cacheImages) {
137 | fs.unlink(imagePath, function(err) {
138 | if (err) {
139 | console.log('error while deleting: ' + imagePath);
140 | }
141 | });
142 | }
143 | });
144 | };
145 |
146 | var serveCachedImageOrGenerate = function(param) {
147 | fs.exists(param.out, function(exists) {
148 | if (exists && !settings.debug) {
149 | serveCachedImage(param.out);
150 | } else {
151 | resizeImage(param.src, param.out, param.args, function() {
152 | serveCachedImage(param.out);
153 | });
154 | }
155 | });
156 | };
157 |
158 | var serveImage = function(params) {
159 | if (typeof params.args === 'undefined') {
160 | //original
161 | serveFile(params.src, undefined);
162 | } else {
163 | serveCachedImageOrGenerate(params);
164 | }
165 | };
166 |
167 | var downloadAndTry = function(params) {
168 | var url = params.remoteUrl;
169 | var output = params.src;
170 | util.downloadAnd(url, output, function(err) {
171 | if (err) {
172 | emitter.emit('error', me.do500, 'Failed to download: '+url);
173 | } else {
174 | serveImage(params);
175 | }
176 | });
177 | };
178 |
179 |
180 | var staticPrefix = '/static/';
181 | var start = function() {
182 | if (request.url.startsWith(staticPrefix)) {
183 | var urlPath = urlutil.parse(request.url).pathname;
184 | var filePath = path.join(staticDir, urlPath.substring(staticPrefix.length));
185 | serveFile(filePath);
186 | } else {
187 |
188 | if (request.method !== 'GET') {
189 | emitter.emit('error', me.do501);
190 | return;
191 | }
192 |
193 | if (request.url === '/favicon.ico') {
194 | emitter.emit('error', me.do404);
195 | return;
196 | }
197 |
198 | var url = request.url;
199 | var parsed = urlParser.parse(request.url);
200 | if (!parsed) {
201 | emitter.emit('error', me.do404);
202 | return;
203 | }
204 | var src = parsed.src;
205 | var isRemoteSrc = !!parsed.remoteUrl;
206 |
207 | fs.exists(src, function(exists) {
208 | if (exists) {
209 | serveImage(parsed);
210 | } else if (isRemoteSrc) {
211 | downloadAndTry(parsed);
212 | } else {
213 | emitter.emit('error', me.do500, 'Image does not exist and cannot be downloaded from remote: '+src);
214 | }
215 | });
216 | }
217 | };
218 |
219 | me.start = function() {
220 | process.nextTick(start);
221 | }
222 |
223 |
224 | return me;
225 | };
226 |
227 |
228 |
229 |
230 | function onEachRequest(req, resp) {
231 | var handler = Handler(req, resp);
232 | handler.emitter.on('error', onError);
233 | handler.start();
234 | }
235 |
236 |
237 | me.server = http.createServer(onEachRequest);
238 |
239 | me.server.on('close', function() {
240 | console.log('Server stopped');
241 | });
242 |
243 | me.start = function(host, port) {
244 | me.stop();
245 | me.server.listen(port, host, function() {
246 | console.log('Server started %s:%d', host, port);
247 | });
248 | };
249 |
250 | me.stop = function() {
251 | try {
252 | me.server.close();
253 | } catch (e) {
254 | }
255 | };
256 |
257 | return me;
258 | };
259 |
260 | function onError(f, msg) {
261 | if (typeof f === 'function') {
262 | if (typeof msg !== 'undefined') {
263 | console.log(msg);
264 | }
265 | f(msg);
266 | }
267 | }
268 |
269 | module.exports = Server;
270 |
--------------------------------------------------------------------------------