-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
207 lines (177 loc) · 5.8 KB
/
index.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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
"use strict";
const PLUGIN_NAME = 'gulp-retina-workflow';
const PLUGIN_DEBUG = false;
// Globals
var gm = require('gm').subClass({ imageMagick: true });
var through = require('through2');
var sizeOf = require('image-size');
var vinylSource = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var path = require('path');
var fs = require('fs');
var defaults = require('defaults');
var PluginError = require('gulp-util').PluginError;
// Main
module.exports = function (args) {
debug(PLUGIN_NAME);
// Process options (set defaults)
const options = defaults(args || {}, {
flags: [
{suffix: '@1x', scale: 1, suffixOut: ''},
{suffix: '@2x', scale: 2, suffixOut: '@2x'},
{suffix: '@3x', scale: 3, suffixOut: '@3x'},
{suffix: '@4x', scale: 4, suffixOut: '@4x'},
],
extensions: ['jpg', 'jpeg', 'png'],
roundUp: true,
quality: 1
});
// Handle file in stream
return through.obj(function (file, encode, callback) {
// Make sure everything is as it should
if (file.isNull()) {
return callback(null, file);
}
if (file.isStream()) {
this.emit('error', new PluginError(PLUGIN_NAME, 'Streams are not supported'));
return callback();
}
// Abort if the file is not of the correct file type
let extension = path.extname(file.path).substr(1);
if (! options.extensions.includes(extension.toLowerCase())) {
debug('File extension filtered out: '+extension+' ('+path.basename(file.path)+')');
return callback(null, file);
}
// Pull apart path and make the info easily accessible
let info = getFileInfo(file);
// Abort if it doesn't have a configured suffix
if (! info.flag) {
debug('No matching flags in file name: '+info.basename);
return callback(null, file);
}
// Add original file to stream (resizing of copies happens after 'end' event)
file.path = info.partial+info.flag.suffixOut+'.'+info.extension;
// Create new images
let streams = [];
for (let set of getWorkList(info)) {
streams.push(resizeImage(set, info));
}
// Add images to stream
if (streams.length) {
// Use the counter to verify when we're done with the last file
let counter = streams.length;
let mainStream = this;
streams.forEach(function(stream) {
// Buffer the stream so that it's in the default gulp format
stream.pipe(buffer()).pipe(through.obj(function(resized, enc, cb) {
// Add resized file to stream
mainStream.push(resized);
// Add original file if we're on the final iteration
if (! --counter) {
mainStream.push(file);
callback();
}
}));
});
}
else {
this.push(file);
callback();
}
});
// Build the list of files we should create
function getWorkList(source) {
let workList = [];
// Get flags in descending order of scale
let flagsDesc = [...options.flags].sort(function(a, b) {
return a.scale + b.scale;
});
for (let flag of flagsDesc) {
if (flag.scale < source.flag.scale) {
// Only add file to workList if there doesn't already exist source files
// of lower resolution (that may be optimized for the size)
if (! fileExists(source.partial+flag.suffix+'.'+source.extension)) {
workList.push({
scale: flag.scale,
target: source.partial+flag.suffixOut+'.'+source.extension,
});
}
else {
// Stop here if smaller copies exist
break;
}
}
}
return workList;
}
// Resize image
function resizeImage(set, source) {
// Calculate new image settings
let quality = clamp(Math.floor(options.quality * 100), 0, 100);
let scale = set.scale / source.flag.scale;
let size = [];
// Calculate new size
if (options.roundUp) {
size = [Math.ceil(source.size.width * scale), Math.ceil(source.size.height * scale)];
}
else {
size = [Math.floor(source.size.width * scale), Math.floor(source.size.height * scale)];
}
// Create image and return the stream
return gm(source.path)
.resize(size[0], size[1])
.quality(quality)
.stream()
.pipe(vinylSource())
.pipe(parseStream(set.target, source.base));
}
// Deconstruct a file path to make information more accessible
function getFileInfo(file) {
// Pul apart path
let extension = path.extname(file.path).substr(1);
let basename = path.basename(file.path);
let name = path.basename(file.path, '.'+extension);
let directory = path.dirname(file.path);
let flag = false;
let size = sizeOf(file.path);
// Map flag to file
for (let currentFlag of options.flags) {
if (name.slice(-currentFlag.suffix.length) === currentFlag.suffix) {
name = name.slice(0, -currentFlag.suffix.length);
flag = currentFlag;
break;
}
}
// This is used to easily create paths for other sizes
let partial = path.join(directory, name);
// Return mega verbose file object
return { extension, basename, name, directory, partial, flag, size, base: file.base, path: file.path };
}
// Clamp a value between a max and min
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Function for synchronously testing if a file exists or not
function fileExists(path) {
try {
fs.accessSync(path, fs.F_OK);
return true;
} catch (e) {
return false;
}
}
// Print to console if we're debugging
function debug(msg) {
if (PLUGIN_DEBUG) {
console.log(msg);
}
}
// Create a stream
function parseStream(path, base) {
return through.obj(function(file, encode, callback){
file.base = base;
file.path = path;
return callback(null, file);
});
}
};