@gld5000-cli/image-scripts
Version:
A useful collection of image processing scripts
275 lines (263 loc) • 8.88 kB
JavaScript
import sharp from "sharp";
import fs from "fs/promises";
import path from "path";
import { selectLineFromText } from "../general/selectLineFromText.mjs";
import { isPngFile, isImageFile } from "../general/fileTypeTests.mjs";
/**
* Loop through list of files with name and path
* Copy image file to new directory
* Convert / process png if needed
* Rename with dimensions/ lowercase etc.
* Return array of new file names with paths
* @param {string[]} fileListFullPath
* @param {boolean} shouldMakeEven
* @returns {Promise<{newFileList: string[], backupList: string[]}>}
*/
export async function renameFiles(fileListFullPath,shouldMakeEven = false) {
try {
const newFileList = [];
const pngStrategy = fileListFullPath.some(
(item) => item.indexOf("png") !== -1
)
? await selectLineFromText(["keep", "convert", "both"])
: "keep";
for (const file of fileListFullPath) {
const isImage = isImageFile(file);
if (isImage) {
const isPng = isPngFile(file);
const movedFile = await processAndCopyFile(file, isPng, pngStrategy,shouldMakeEven);
for (const file of movedFile) {
const newFileName = await fileNameDimensions(file);
newFileList.push(newFileName);
await fs.rename(file, newFileName);
}
}
}
return { newFileList };
} catch (error) {
console.log("Error renaming", error);
throw error;
}
}
/**
* Take in filename with path
* Create 'processed' directory (if does not exist)
* Copy files to 'processed' directory
* @param {string[]} fileListFullPath
* @returns {Promise<string>}
*/
export async function copyFileToSubFolder(
fileFullPath,
folderName = "processed"
) {
const pathName = path.dirname(fileFullPath);
const baseName = path.basename(fileFullPath);
const origin = fileFullPath;
const newPath = path.join(pathName, folderName);
const destination = path.join(newPath, baseName);
try {
await fs.access(newPath);
} catch (error) {
await fs.mkdir(newPath);
}
try {
await fs.copyFile(origin, destination);
} catch (error) {
console.log("error copying file:", error);
}
return destination;
}
/**
* Take in filename with path
* Copy files to 'processed' directory
* Convert PNG to JPG and delete PNG as needed
* @param {string} fileFullPath
* @param {boolean} isPng
* @param {string} pngStrategy
* @param {boolean} shouldMakeEven
* @returns {Promise<string[]>}
*/
async function processAndCopyFile(
fileFullPath,
isPng,
pngStrategy,
shouldMakeEven = false
) {
shouldMakeEven && await makeImageEven(fileFullPath);
const workingFile = await copyFileToSubFolder(fileFullPath);
if (!isPng || pngStrategy === "keep") return [workingFile];
const shouldDelete = pngStrategy === "convert";
return await convertPngToJpg(workingFile, shouldDelete);
}
/**
* Take in filename with path
* Create 'processed' directory (if does not exist)
* Copy files to 'processed' directory
* @param {string[]} fileListFullPath
* @param {string[]} destinationDirName
* @returns {Promise<string>}
*/
export async function copyFileToNewFolder(fileFullPath, destinationDirName) {
const origin = fileFullPath;
const baseName = path.basename(fileFullPath);
const destination = path.join(destinationDirName, baseName);
try {
await fs.access(destinationDirName);
} catch (error) {
await fs.mkdir(destinationDirName);
}
try {
await fs.copyFile(origin, destination);
} catch (error) {
console.log("error copying file:", error);
}
return destination;
}
/**
* Take in file with full path
* Convert from PNG to JPG
* Delete the original JPG
* @param {string} file
* @param {boolean} shouldDeletePng
* @returns {Promise<string[]>}
*/
async function convertPngToJpg(file, shouldDeletePng = true) {
try {
// Extract the directory path and filename without extension
const dirPath = path.dirname(file);
const fileNameWithoutExt = path.basename(file, ".png");
if (!shouldDeletePng) {
await cropImageToContent(file);
}
// Create the output JPG filename
const jpgFilePath = path.join(dirPath, `${fileNameWithoutExt}.jpg`);
// Convert PNG to JPG
await sharp(file).jpeg({ quality: 80 }).toFile(jpgFilePath);
//console.log(`Converted ${file} to JPG: ${jpgFilePath}`);
// Delete the original PNG file
if (shouldDeletePng) {
await fs.unlink(file);
//console.log(`Deleted original PNG file: ${file}`);
}
return shouldDeletePng ? [jpgFilePath] : [jpgFilePath, file];
} catch (error) {
console.error(`Error converting ${file}:`, error);
throw error; // Re-throw the error so it can be caught by the caller
}
}
/**
* Take filename with full path
* Return component parts
* @param {string} fileNameFullPath
* @returns {string}
*/
function fullPathNameSplitter(fileNameFullPath) {
const pathName = path.dirname(fileNameFullPath);
const baseName = path.basename(fileNameFullPath);
const extName = path.extname(fileNameFullPath);
const baseNameNoExt = path.basename(
fileNameFullPath,
path.extname(fileNameFullPath)
);
return { pathName, baseName, baseNameNoExt, extName };
}
/**
* Take filename with full path
* Cleanup filename
* Add image dimensions
* @param {string} fileNameFullPath
* @returns {Promise<string>}
*/
export async function fileNameDimensions(fileNameFullPath) {
try {
const { width, height } = await getImageDimensions(fileNameFullPath);
const { pathName, baseNameNoExt, extName } =
fullPathNameSplitter(fileNameFullPath);
const newFileName = `${processFilename(
baseNameNoExt
)}_${width}_${height}${extName}`;
const newNameFullPath = path.join(pathName, newFileName);
return newNameFullPath;
} catch (error) {
console.error(`Error processing ${fileNameFullPath}:`, error);
}
}
/**
* Take filename with full path
* Cleanup filename
* * @param {string} fileNameFullPath
* @returns {string}
*/
function processFilename(filename) {
const cleanedName = filename
.replaceAll(/[-\s]+/g, "-")
.replace(/([\_x\-][\d]+)+$/g, "")
.toLowerCase();
//console.log('cleanedName:', cleanedName);
return moveBreakpointToFilenameEnd(cleanedName).replaceAll(/[_-]+/g, "-");
}
/**
* Take cleaned filename
* Move breakpoint name to end of filename
* * @param {string} fileNameFullPath
* @returns {string}
*/
function moveBreakpointToFilenameEnd(filename) {
const breakpointMatch = filename.match(
/([\-]?mobile)|([\-]?tablet)|([\-]?desktop)|([\-]?phone)|([\-]?phablet)/
);
const breakpoint = breakpointMatch ? breakpointMatch[0] : undefined;
//console.log('breakpoint:', breakpoint);
return breakpoint
? `${filename.replace(breakpoint, "").replace(/^\-/, "")}-${breakpoint}`
: filename;
}
/**
*
* @param {string} fileNameFullPath
* @returns {Promise<{[key:string]:string}>}
*/
export async function getImageDimensions(fileNameFullPath) {
const sharpInstance = sharp(fileNameFullPath);
const metadata = await sharpInstance.metadata();
const { width, height } = metadata;
return { width, height };
}
export async function cropInputFrame(file, width, height) {
const oldFile = await copyFileToSubFolder(file, "old");
await sharp(oldFile).extract({ left: 0, top: 0, width, height }).toFile(file);
}
export async function getEvenImageDimensions(fileNameFullPath) {
const sharpInstance = sharp(fileNameFullPath);
const metadata = await sharpInstance.metadata();
const { width, height } = metadata;
const widthEven = width % 2 === 0 ? width : width - 1;
const heightEven = height % 2 === 0 ? height : height - 1;
const shouldCrop = width !== widthEven || height !== heightEven;
return { width: widthEven, height: heightEven, shouldCrop };
}
async function cropImageToContent(inputPath) {
const tempPath = appendFilename(inputPath, "old");
//console.log('tempPath:', tempPath);
// rename old file
await fs.rename(inputPath, tempPath);
// trim
await sharp(tempPath).trim().toFile(inputPath);
// delete old file
await fs.unlink(tempPath);
}
/**
*
* @param {string} filename
* @param {string} appendString
* @returns {string}
*/
function appendFilename(filename, appendString) {
const { pathName, baseNameNoExt, extName } = fullPathNameSplitter(filename);
const newPath = `${pathName}${baseNameNoExt}${appendString}${extName}`;
return newPath;
}
export async function makeImageEven(file) {
const { width, height, shouldCrop } = await getEvenImageDimensions(file);
shouldCrop && (await cropInputFrame(file, width, height));
}