Image Optimization Script (#348)
A build-time script to optimize images. Note that going forward, all images should be placed in `images` instead of `public/images`. Co-authored-by: Amy <a258wang@uwaterloo.ca> Reviewed-on: #348 Reviewed-by: Aditya Thakral <a3thakra@csclub.uwaterloo.ca> Reviewed-by: n3parikh <n3parikh@csclub.uwaterloo.ca> Co-authored-by: Amy <a258wang@csclub.uwaterloo.ca> Co-committed-by: Amy <a258wang@csclub.uwaterloo.ca>pull/358/head
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 318 B |
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 475 B After Width: | Height: | Size: 475 B |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 836 B |
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 425 B |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 627 B |
Before Width: | Height: | Size: 365 B After Width: | Height: | Size: 365 B |
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 227 B |
Before Width: | Height: | Size: 190 B After Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 220 B After Width: | Height: | Size: 220 B |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 877 KiB After Width: | Height: | Size: 877 KiB |
Before Width: | Height: | Size: 907 KiB After Width: | Height: | Size: 907 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
Before Width: | Height: | Size: 4.2 MiB After Width: | Height: | Size: 4.2 MiB |
Before Width: | Height: | Size: 786 KiB After Width: | Height: | Size: 786 KiB |
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 200 KiB |
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 847 KiB After Width: | Height: | Size: 847 KiB |
Before Width: | Height: | Size: 968 KiB After Width: | Height: | Size: 968 KiB |
Before Width: | Height: | Size: 733 KiB After Width: | Height: | Size: 733 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 336 KiB |
Before Width: | Height: | Size: 461 KiB After Width: | Height: | Size: 461 KiB |
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 277 KiB |
Before Width: | Height: | Size: 862 KiB After Width: | Height: | Size: 862 KiB |
Before Width: | Height: | Size: 1012 KiB After Width: | Height: | Size: 1012 KiB |
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 526 KiB |
Before Width: | Height: | Size: 454 KiB After Width: | Height: | Size: 454 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 177 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 897 KiB After Width: | Height: | Size: 897 KiB |
Before Width: | Height: | Size: 785 KiB After Width: | Height: | Size: 785 KiB |
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 341 KiB |
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 738 KiB After Width: | Height: | Size: 738 KiB |
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 320 KiB |
Before Width: | Height: | Size: 377 B After Width: | Height: | Size: 377 B |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,107 @@ |
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ |
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */ |
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ |
||||
|
||||
// TODO: upgrade libsquoosh once types are available: https://github.com/GoogleChromeLabs/squoosh/issues/1077
|
||||
|
||||
import { cpus } from "os"; |
||||
import path from "path"; |
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import { ImagePool } from "@squoosh/lib"; |
||||
import fse from "fs-extra"; |
||||
import { default as getImageDimensions } from "image-size"; |
||||
|
||||
const SOURCE_DIRECTORY = "images"; |
||||
const DESTINATION_DIRECTORY = path.join("public", "images"); |
||||
|
||||
// directory where Meet the Team headshots are stored, relative to the source directory
|
||||
const TEAM_IMAGES_DIRECTORY = path.join("team", ""); |
||||
|
||||
const IMAGE_MINIMUM_SIZE = 512; |
||||
|
||||
const GET_ENCODER_FROM_EXTENSION: { [imageExtension: string]: string } = { |
||||
jpg: "mozjpeg", |
||||
jpeg: "mozjpeg", |
||||
png: "oxipng", |
||||
}; |
||||
|
||||
const ENCODER_OPTIONS: { [encoder: string]: Record<string, unknown> } = { |
||||
mozjpeg: {}, |
||||
oxipng: {}, |
||||
}; |
||||
|
||||
void optimizeImages(); |
||||
|
||||
export async function optimizeImages() { |
||||
const imagePaths = await getFilePathsInDirectory(SOURCE_DIRECTORY); |
||||
await fse.emptyDir(DESTINATION_DIRECTORY); |
||||
|
||||
// maximum number of workers is 8 in order to avoid running out of memory
|
||||
const numberOfWorkers = Math.min(cpus().length, 8); |
||||
const imagePool = new ImagePool(numberOfWorkers); |
||||
|
||||
await Promise.all( |
||||
imagePaths.map(async (imagePath) => { |
||||
const sourcePath = path.join(SOURCE_DIRECTORY, imagePath); |
||||
const destinationPath = path.join(DESTINATION_DIRECTORY, imagePath); |
||||
const fileExtension = imagePath.split(".").pop() ?? ""; |
||||
const encoder = GET_ENCODER_FROM_EXTENSION[fileExtension]; |
||||
|
||||
if (!encoder) { |
||||
await fse.copy(sourcePath, destinationPath); |
||||
return; |
||||
} |
||||
|
||||
const rawImageFile = await fse.readFile(sourcePath); |
||||
const ingestedImage = imagePool.ingestImage(rawImageFile); |
||||
const { width, height } = getImageDimensions(rawImageFile); |
||||
|
||||
await ingestedImage.decoded; |
||||
|
||||
const shouldResize = |
||||
imagePath.startsWith(TEAM_IMAGES_DIRECTORY) && |
||||
(width ?? 0) > IMAGE_MINIMUM_SIZE && |
||||
(height ?? 0) > IMAGE_MINIMUM_SIZE; |
||||
|
||||
if (width && height && shouldResize) { |
||||
const smallerDimension = width < height ? "width" : "height"; |
||||
|
||||
// specifying only one dimension maintains the aspect ratio
|
||||
const preprocessOptions = { |
||||
resize: { |
||||
enabled: true, |
||||
[smallerDimension]: IMAGE_MINIMUM_SIZE, |
||||
}, |
||||
}; |
||||
|
||||
await ingestedImage.preprocess(preprocessOptions); |
||||
} |
||||
|
||||
const encodeOptions = { [encoder]: ENCODER_OPTIONS[encoder] }; |
||||
await ingestedImage.encode(encodeOptions); |
||||
|
||||
const encodedImage = await ingestedImage.encodedWith[encoder]; |
||||
await fse.outputFile(destinationPath, encodedImage.binary); |
||||
}) |
||||
); |
||||
|
||||
await imagePool.close(); |
||||
} |
||||
|
||||
async function getFilePathsInDirectory(directory: string): Promise<string[]> { |
||||
const entries = await fse.readdir(directory, { withFileTypes: true }); |
||||
return ( |
||||
await Promise.all( |
||||
entries.map(async (entry) => { |
||||
if (entry.isDirectory()) { |
||||
const subdirectory = path.join(directory, entry.name); |
||||
const subentries = await getFilePathsInDirectory(subdirectory); |
||||
return subentries.map((subentry) => path.join(entry.name, subentry)); |
||||
} |
||||
return entry.name; |
||||
}) |
||||
) |
||||
).flat(); |
||||
} |