|
|
|
@ -16,14 +16,16 @@ 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
|
|
|
|
|
// directories are relative to SOURCE_DIRECTORY
|
|
|
|
|
const TEAM_IMAGES_DIRECTORY = path.join("team", ""); |
|
|
|
|
const EVENTS_IMAGES_DIRECTORY = path.join("events", ""); |
|
|
|
|
|
|
|
|
|
const IMAGE_MINIMUM_SIZE = 512; |
|
|
|
|
|
|
|
|
|
const GET_ENCODER_FROM_EXTENSION: { [imageExtension: string]: string } = { |
|
|
|
|
jpg: "mozjpeg", |
|
|
|
|
jpeg: "mozjpeg", |
|
|
|
|
JPG: "mozjpeg", |
|
|
|
|
png: "oxipng", |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
@ -35,6 +37,8 @@ const ENCODER_OPTIONS: { [encoder: string]: Record<string, unknown> } = { |
|
|
|
|
void optimizeImages(); |
|
|
|
|
|
|
|
|
|
export async function optimizeImages() { |
|
|
|
|
const startTime = Date.now(); |
|
|
|
|
|
|
|
|
|
const imagePaths = await getFilePathsInDirectory(SOURCE_DIRECTORY); |
|
|
|
|
await fse.emptyDir(DESTINATION_DIRECTORY); |
|
|
|
|
|
|
|
|
@ -42,52 +46,72 @@ export async function optimizeImages() { |
|
|
|
|
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]; |
|
|
|
|
const batchSize = 40; |
|
|
|
|
|
|
|
|
|
for (let i = 0; i < imagePaths.length; i += batchSize) { |
|
|
|
|
await Promise.all( |
|
|
|
|
imagePaths.slice(i, i + batchSize).map(async (imagePath) => { |
|
|
|
|
const imageStartTime = Date.now(); |
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
console.log( |
|
|
|
|
`Copied ${imagePath} in ${getElapsedSeconds(imageStartTime)}s` |
|
|
|
|
); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const rawImageFile = await fse.readFile(sourcePath); |
|
|
|
|
const ingestedImage = imagePool.ingestImage(rawImageFile); |
|
|
|
|
const { width, height } = getImageDimensions(rawImageFile); |
|
|
|
|
|
|
|
|
|
if (!encoder) { |
|
|
|
|
await fse.copy(sourcePath, destinationPath); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
await ingestedImage.decoded; |
|
|
|
|
|
|
|
|
|
const rawImageFile = await fse.readFile(sourcePath); |
|
|
|
|
const ingestedImage = imagePool.ingestImage(rawImageFile); |
|
|
|
|
const { width, height } = getImageDimensions(rawImageFile); |
|
|
|
|
const shouldResize = |
|
|
|
|
(imagePath.startsWith(TEAM_IMAGES_DIRECTORY) || |
|
|
|
|
imagePath.startsWith(EVENTS_IMAGES_DIRECTORY)) && |
|
|
|
|
(width ?? 0) > IMAGE_MINIMUM_SIZE && |
|
|
|
|
(height ?? 0) > IMAGE_MINIMUM_SIZE; |
|
|
|
|
|
|
|
|
|
await ingestedImage.decoded; |
|
|
|
|
if (width && height && shouldResize) { |
|
|
|
|
const smallerDimension = width < height ? "width" : "height"; |
|
|
|
|
|
|
|
|
|
const shouldResize = |
|
|
|
|
imagePath.startsWith(TEAM_IMAGES_DIRECTORY) && |
|
|
|
|
(width ?? 0) > IMAGE_MINIMUM_SIZE && |
|
|
|
|
(height ?? 0) > IMAGE_MINIMUM_SIZE; |
|
|
|
|
// specifying only one dimension maintains the aspect ratio
|
|
|
|
|
const preprocessOptions = { |
|
|
|
|
resize: { |
|
|
|
|
enabled: true, |
|
|
|
|
[smallerDimension]: IMAGE_MINIMUM_SIZE, |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if (width && height && shouldResize) { |
|
|
|
|
const smallerDimension = width < height ? "width" : "height"; |
|
|
|
|
await ingestedImage.preprocess(preprocessOptions); |
|
|
|
|
|
|
|
|
|
// specifying only one dimension maintains the aspect ratio
|
|
|
|
|
const preprocessOptions = { |
|
|
|
|
resize: { |
|
|
|
|
enabled: true, |
|
|
|
|
[smallerDimension]: IMAGE_MINIMUM_SIZE, |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
console.log( |
|
|
|
|
`Resized ${sourcePath} in ${getElapsedSeconds(imageStartTime)}s` |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await ingestedImage.preprocess(preprocessOptions); |
|
|
|
|
} |
|
|
|
|
const encodeOptions = { [encoder]: ENCODER_OPTIONS[encoder] }; |
|
|
|
|
await ingestedImage.encode(encodeOptions); |
|
|
|
|
|
|
|
|
|
const encodeOptions = { [encoder]: ENCODER_OPTIONS[encoder] }; |
|
|
|
|
await ingestedImage.encode(encodeOptions); |
|
|
|
|
const encodedImage = await ingestedImage.encodedWith[encoder]; |
|
|
|
|
await fse.outputFile(destinationPath, encodedImage.binary); |
|
|
|
|
|
|
|
|
|
const encodedImage = await ingestedImage.encodedWith[encoder]; |
|
|
|
|
await fse.outputFile(destinationPath, encodedImage.binary); |
|
|
|
|
}) |
|
|
|
|
); |
|
|
|
|
console.log( |
|
|
|
|
`Optimized ${sourcePath} in ${getElapsedSeconds(imageStartTime)}s` |
|
|
|
|
); |
|
|
|
|
}) |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await imagePool.close(); |
|
|
|
|
|
|
|
|
|
console.log(`TOTAL DURATION: ${getElapsedSeconds(startTime)}s`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function getFilePathsInDirectory(directory: string): Promise<string[]> { |
|
|
|
@ -105,3 +129,7 @@ async function getFilePathsInDirectory(directory: string): Promise<string[]> { |
|
|
|
|
) |
|
|
|
|
).flat(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getElapsedSeconds(startTime: number) { |
|
|
|
|
return (Date.now() - startTime) / 1000; |
|
|
|
|
} |
|
|
|
|