Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
317 lines (272 sloc) 7.46 KB
'use strict';
const _ = require('lodash');
const crypto = require('crypto');
const fs = require('fs');
const globby = require('globby');
const Jimp = require('jimp');
const multimatch = require('multimatch');
const mv = require('mv');
const path = require('path');
const pify = require('pify');
const imagemin = require('./lib/imagemin');
const urlBase64 = require('./lib/url-base64')
const fsP = pify(fs);
const options = {
source: path.join(__dirname, 'src'),
destination: path.join(__dirname, 'i'),
pattern: ['**/*.+(jpg|jpeg|gif|png)'],
densities: [1, 2],
sizes: [660],
gradientStep: 0.2,
knownSizes: {
32: '0', 64: '1',
192: '2', 384: '3',
200: '4', 400: '5',
300: '6', 600: '7',
360: '8', 720: '9',
660: 'a', 1320: 'b'
}
};
function mapPromise(array, promiseFactory) {
const results = []
return array.reduce(
(p, item) => p.then(() => promiseFactory(item)).then(r => results.push(r)),
Promise.resolve()
).then(() => results);
}
function tryLoadJson(p) {
return fsP.readFile(p, 'utf8')
.then(str => JSON.parse(str))
.catch(err => { return {} });
}
function digest(p) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5');
const fd = fs.createReadStream(p);
fd.on('end', () => {
hash.setEncoding('base64');
hash.end();
const digest = hash.read();
const encoded = urlBase64.encode(digest).substr(0, 10)
resolve(encoded);
});
fd.pipe(hash);
});
}
function loadImageMeta(p) {
const absPath = path.join(options.source, p);
return Promise.all([
tryLoadJson(absPath + '.meta.json'),
digest(absPath)
]).then(values => _.assign(
{
sizes: options.sizes,
densities: options.densities
},
values[0],
{
digest: values[1],
pathInfo: path.parse(p),
path: p,
absPath: path.join(options.source, p)
}
));
}
function generateSizeIf(rx, sizes) {
return i => {
if (!rx.test(i.path)) {
return i;
}
return _.assign({}, i, {
sizes: _.uniq(_.concat(i.sizes, sizes))
});
}
}
function generateImageGradient(jimp) {
function averageChunk(left, width) {
const color = jimp.clone()
.crop(left, 0, width, jimp.bitmap.height)
.resize(1, 1, Jimp.RESIZE_BICUBIC)
.getPixelColor(0, 0)
return Jimp.intToRGBA(color)
}
function rgbToHex({ r, g, b }) {
function convert(c) {
const hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
return '#' + convert(r) + convert(g) + convert(b);
}
const gradientStepPixels = options.gradientStep * jimp.bitmap.width
const stops = []
for (
let px = 0, pct = 0;
pct <= 1;
px += gradientStepPixels, pct += options.gradientStep
) {
const rgb = averageChunk(px, gradientStepPixels)
stops.push({
position: (pct * 100).toFixed(),
color: rgbToHex(rgb)
})
}
return 'linear-gradient(90deg, ' +
stops.map(({ color, position }) => `${color} ${position}%`).join(', ') +
')'
}
function loadImageData(image) {
return Jimp.read(image.absPath).then(jimp => {
image.aspectRatio = jimp.bitmap.height / jimp.bitmap.width
return generateImageGradient(jimp)
}).then(gradient => {
image.gradient = gradient
return image
})
}
function resizeImage(i, size, density) {
let suffix = '';
const newSize = size * density;
if (newSize in options.knownSizes) {
suffix += options.knownSizes[newSize];
} else {
suffix += `${newSize}`;
}
const newExt = /^\.[Pp][Nn][Gg]$/.test(i.pathInfo.ext)
? '.jpeg'
: i.pathInfo.ext
const newName = `${i.digest}${suffix}${newExt}`
const newPath = path.join(options.destination, newName);
const result = {
image: i,
size: size,
density: density,
name: newName,
path: newPath
}
return fsP.stat(newPath)
.then(stat => {
result.originalSize = stat.size;
result.compressedSize = stat.size;
return result;
})
.catch(err => {
const absPath = path.join(options.source, i.path);
console.log(`resize ${i.path} to ${newSize} => ${newName}`);
return Jimp.read(absPath).then(image => {
return new Promise((resolve, reject) => {
return image.resize(newSize, Jimp.AUTO).quality(100).write(
newPath,
err => err ? reject(err) : resolve(result)
)
});
});
});
}
function createWebpImage(result) {
const p = path.parse(result.path);
p.ext = '.webp';
p.base = p.name + p.ext;
result.webpPath = path.format(p);
result.webpName = p.base;
return fsP.stat(result.webpPath)
.then(stat => {
result.webpSize = stat.size;
return result;
})
.catch(err => {
return imagemin.webp([result.path]).then(webp => {
result.webpSize = webp[0].data.length;
return fsP.writeFile(result.webpPath, webp[0].data);
})
})
.then(() => result);
}
function compressImage(result) {
if (result.compressedSize) {
return Promise.resolve(result);
}
return fsP.stat(result.path).then(stat => {
result.originalSize = stat.size;
return imagemin([result.path]);
}).then(compressed => {
result.compressedSize = compressed[0].data.length;
return fsP.writeFile(result.path, compressed[0].data);
}).then(() => result);
}
function resizeAllImages(images) {
const flat = _.flatMapDeep(images, i => {
return i.sizes.map(size => {
return i.densities.map(density => {
return { i, size, density };
})
})
})
return mapPromise(flat, x => {
return resizeImage(x.i, x.size, x.density)
.then(createWebpImage)
.then(compressImage);
})
}
function createImageMap(results) {
const imageMap = _.mapValues(
_.groupBy(results, r => r.image.path),
byName => ({
aspectRatio: byName[0].image.aspectRatio,
gradient: byName[0].image.gradient,
sizes: _.mapValues(
_.groupBy(byName, 'size'),
bySize => _.mapValues(
_.groupBy(bySize, 'density'),
byDensity => ({
name: byDensity[0].name,
webpName: byDensity[0].webpName
})
)
)
})
);
return fsP.writeFile(
path.join(options.source, 'img-map.json'),
JSON.stringify(imageMap)
);
}
function getObsoleteImages(results) {
return fsP.readdir(options.destination).then(
files => _.difference(
_.without(files, '.gitignore'),
_.map(results, 'name').concat(
_.map(results, 'webpName')
)
)
);
}
function removeObsoleteImages(obsolete) {
return mapPromise(obsolete, p => {
const absPath = path.join(options.destination, p);
console.log(`remove: ${absPath}`)
return fsP.unlink(absPath);
});
}
if (!fs.existsSync(options.destination)) {
fs.mkdirSync(options.destination)
}
globby(options.pattern, { cwd: options.source })
.then(paths => mapPromise(paths, loadImageMeta))
.then(images => mapPromise(images, loadImageData))
.then(images => images.map(generateSizeIf(/\/index\.[^.]+$/, [200])))
.then(resizeAllImages)
.then(results => {
const originalSize = _.sumBy(results, 'originalSize');
const compressedSize = _.sumBy(results, 'compressedSize');
const webpSize = _.sumBy(results, 'webpSize');
console.log({
originalSize,
compressedSize,
compressedDelta: originalSize - compressedSize,
webpDelta: originalSize - webpSize
});
return createImageMap(results)
.then(() => getObsoleteImages(results))
.then(removeObsoleteImages);
})
.catch(console.error);