-
Notifications
You must be signed in to change notification settings - Fork 2
/
optimize-img.js
124 lines (103 loc) Β· 3.5 KB
/
optimize-img.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
const sharp = require("sharp")
const path = require("path")
const {promises: fs} = require("fs")
const R = require("ramda")
const sourceDir = "public/img"
const destDir = "public/optimized-img"
const notEndsWith = extension => R.filter(node => R.not(R.endsWith(extension, node)))
const filterUnwantedExtensions = R.compose(
notEndsWith(".svg"),
notEndsWith(".gif"),
notEndsWith(".DS_Store")
)
const walkDirs = source => R.map(async node => {
const nextPath = path.join(source, node)
const stat = await fs.stat(nextPath)
return stat.isDirectory() ? await lsFiles(nextPath) : nextPath
})
/**
* Recursively get a list of all leaf file paths in a directory
*/
const lsFiles = async source => {
const contents = await fs.readdir(source)
const transducer = R.compose(filterUnwantedExtensions, walkDirs(source))
const files = R.transduce(transducer,
R.flip(R.append),
[], contents)
return R.flatten(await Promise.all(files))
}
const computePathAndName = R.map(f => {
const components = f.split("/")
return {filePath: R.join("/", R.dropLast(1, components)),
name: R.last(components)}
})
const appendOptimizedPath = R.map(pathAndName => (
{...pathAndName,
optimizedPath: path.join(
R.replace(sourceDir, destDir, pathAndName.filePath),
pathAndName.name)
}
))
const tentativeOptimizedPaths = imgFiles => {
const transducer = R.compose(computePathAndName, appendOptimizedPath)
return R.transduce(transducer, R.flip(R.append), [], imgFiles)
}
/**
* Remove the processed file names like w-20.jpeg
* and return just the path
*/
const stripFileNames = processedImgFiles => {
const txf = R.compose(
R.join("/"),
R.dropLast(1),
R.split("/")
)
return R.uniq(R.map(txf, processedImgFiles))
}
const filesToProcess = (imgFiles, processedImgFiles) => {
const computedOutComponents = tentativeOptimizedPaths(imgFiles)
const existingOptimizedPaths = stripFileNames(processedImgFiles)
return R.filter(
c => R.not(R.includes(c.optimizedPath, existingOptimizedPaths)),
computedOutComponents
)
}
const widthsToGenerate = [80, 240, 480, 720, 960, 1440]
const widthsToBlur = [480, 960]
const processFile = async component => {
// create the folder
await fs.mkdir(component.optimizedPath, {recursive: true})
// convert image to webp format in highest quality
const inputSharp = sharp(path.join(component.filePath, component.name))
const ogWebpPath = path.join(component.optimizedPath, "og.webp")
await inputSharp.toFormat("webp").webp({lossless: true}).toFile(ogWebpPath)
// convert og.webp to desired widths
const ogSharp = sharp(ogWebpPath)
await Promise.all(R.map(async width => {
const widthPath = path.join(component.optimizedPath, `w-${width}.webp`)
await ogSharp.clone().resize({width}).toFile(widthPath)
// blur if needed
if (R.contains(width, widthsToBlur)) {
await sharp(widthPath).blur(8).toFile(path.join(component.optimizedPath, `w-${width}-blurred.webp`))
}
}, widthsToGenerate))
}
async function main() {
const imgFiles = await lsFiles(sourceDir)
const processedImgFiles = await lsFiles(destDir)
const unprocessedImageFiles = filesToProcess(imgFiles, processedImgFiles)
try {
await Promise.all(R.map(await processFile, unprocessedImageFiles))
console.log("Images optimized successfully!")
} catch(e) {
console.error(e)
}
}
module.exports = {
filterUnwantedExtensions, computePathAndName,
sourceDir, destDir, appendOptimizedPath,
stripFileNames, filesToProcess
}
if (require.main === module) {
main()
}