-
Notifications
You must be signed in to change notification settings - Fork 21
/
retinajs.js
158 lines (135 loc) · 5.77 KB
/
retinajs.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
'use strict';
require('bluebird');
const _ = require('lodash'),
debug = require('@tryghost/debug')('plugins:retinajs'),
maxSupportedDpr = 5,
sizeOf = require('image-size');
class RetinaJS {
constructor(uploader, uploaderOptions, retinajsOptions) {
this.uploader = uploader;
this.uploaderOptions = uploaderOptions || {};
this.retinajsOptions = retinajsOptions || {};
this.retinajsOptions.baseWidth = parseInt(this.retinajsOptions.baseWidth, 10);
this.retinajsOptions.fireForget = this.retinajsOptions.fireForget || false;
debug('constructor:retinajsOptions', this.retinajsOptions);
if (typeof this.uploader !== 'function') {
throw new TypeError('RetinaJS: uploader must be callable');
}
if (typeof this.uploaderOptions.upload === 'undefined' ||
typeof this.uploaderOptions.upload.public_id === 'undefined' ||
this.uploaderOptions.upload.public_id.length === 0
) {
throw new TypeError('RetinaJS error: invalid uploaderOptions.upload.public_id. Ensure to enable Cloudinary upload.use_filename option.');
}
if (isNaN(this.retinajsOptions.baseWidth)) {
throw new TypeError('RetinaJS config error: invalid retinajs.baseWidth option');
}
if (this.retinajsOptions.baseWidth < 1) {
throw new RangeError('RetinaJS config error: retinajs.baseWidth must be >= 1');
}
}
/**
* Generates and creates the RetinaJS variants for given image
* @param {object} image The image object to retinize
* @return {Promise} A Promise
*/
retinize(image) {
const that = this,
[head, ...tail] = this.generateDprConfigs(this.resolveMaxDpr(image.path));
debug('retinize:configs', {
head: head,
tail: tail
});
// Image is not retinizable: creates DPR 1.0 variant only
if (tail.length === 0) {
return that.uploader(image.path, head, true);
}
// Creates the highest DPR variant first then creates subsequent variants
return that.uploader(image.path, head, true).
then((url) => {
const variants = _.map(tail, (c) => that.uploader(url, c, false)),
// First creation call returns URL for highest DPR, in the post editor
// we need the DPR 1.0 variant (RetinaJS identifier-free) URL
finalUrl = that.sanitize(url);
// Creates subsequent variants and returns URL regardless their fulfillment status
if (that.retinajsOptions.fireForget) {
Promise.all(variants).catch((err) => {
console.error(new Error(`Fire&Forget RetinaJS: ${err}`));
});
return finalUrl;
}
// Waits for all subsequent variants to be done then returns the URL
return Promise.all(variants).then(() => finalUrl);
});
}
/**
* Removes the latest RetinaJS identifier (@{i}x) from the given string
* @param {string} string A string
* @return {string} The sanitized string
*/
sanitize(string) {
return decodeURIComponent(string).replace(/@\dx(?!.*@\dx)/, '');
}
/**
* Resolves the max DPR index available for given filename and baseWidth configuration.
* If baseWidth configuration is set to 800 and filename image has a width of 2500,
* the value returned by this method will be 2500 / 800 = 3.125 => 3.
* @param {string} filename Image filename
* @return {int} Max available DPR index or:
* - 1 if image is smaller than baseWidth
* - maxSupportedDpr if image max DPR is higher than Cloudinary can support
*/
resolveMaxDpr(filename) {
const dim = sizeOf(filename),
mdpr = Math.floor(dim.width / this.retinajsOptions.baseWidth);
if (mdpr === 0) {
return 1;
}
if (mdpr > maxSupportedDpr) {
return maxSupportedDpr;
}
return mdpr;
}
/**
* Generates a collection of upload options derivated from the original
* upload otions for each variant in desc mode (highest DPR on the top).
* @param {int} dpr The highest DPR value
* @return {array} A collection customized upload options for all DPRs
*/
generateDprConfigs(dpr) {
if (dpr < 1) {
throw new RangeError(`Unexpected dpr value: ${dpr}`);
}
const configs = [];
for (let i = dpr; i >= 1; i -= 1) {
// Deep clone
const config = JSON.parse(JSON.stringify(Object.assign({}, this.uploaderOptions))),
dprConfig = {
// Forces the image width to baseWidth
width: this.retinajsOptions.baseWidth,
// No scale-up!
if: `iw_gt_${this.retinajsOptions.baseWidth}`,
// Resizing method
crop: 'scale',
// The DPR will resize the image accordingly to its value
dpr: `${i}.0`,
// Tags the DPR index so you can browse the DPRs easily
tags: [`dpr${i}`]
};
// Builds the RetinaJS identifier (@{i}x) for variants
// with DPR > 1.0
if (i > 1) {
dprConfig.public_id = `${config.upload.public_id}@${i}x`;
}
_.mergeWith(config.upload, dprConfig, (objv, srcv) => {
if (_.isArray(objv)) {
return objv.concat(srcv);
}
return srcv;
});
configs.push(config);
}
return configs;
}
}
module.exports = RetinaJS;