-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from 343dev/improvement/refactoring
Refactoring
- Loading branch information
Showing
44 changed files
with
1,037 additions
and
890 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ | |
!lib/ | ||
!.optimiztrc.cjs | ||
!cli.js | ||
!convert.js | ||
!index.js | ||
!LICENSE | ||
!optimize.js | ||
!package*.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
import fs from 'node:fs'; | ||
import os from 'node:os'; | ||
import path from 'node:path'; | ||
|
||
import execBuffer from 'exec-buffer'; | ||
import gif2webp from 'gif2webp-bin'; | ||
import pLimit from 'p-limit'; | ||
import sharp from 'sharp'; | ||
|
||
import { calculateRatio } from './lib/calculate-ratio.js'; | ||
import { checkPathAccessibility } from './lib/check-path-accessibility.js'; | ||
import { createProgressBarContainer } from './lib/create-progress-bar-container.js'; | ||
import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; | ||
import { formatBytes } from './lib/format-bytes.js'; | ||
import { getPlural } from './lib/get-plural.js'; | ||
import { getRelativePath } from './lib/get-relative-path.js'; | ||
import { | ||
LOG_TYPES, | ||
log, | ||
logProgress, | ||
logProgressVerbose, | ||
} from './lib/log.js'; | ||
import { optionsToArguments } from './lib/options-to-arguments.js'; | ||
import { parseImageMetadata } from './lib/parse-image-metadata.js'; | ||
import { programOptions } from './lib/program-options.js'; | ||
import { showTotal } from './lib/show-total.js'; | ||
|
||
export async function convert({ filePaths, config }) { | ||
const { | ||
isForced, | ||
isLossless, | ||
shouldConvertToAvif, | ||
shouldConvertToWebp, | ||
} = programOptions; | ||
|
||
const filePathsCount = filePaths.length; | ||
|
||
if (!filePathsCount) { | ||
return; | ||
} | ||
|
||
log(`Converting ${filePathsCount} ${getPlural(filePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})...`); | ||
|
||
const progressBarTotal = shouldConvertToAvif && shouldConvertToWebp | ||
? filePathsCount * 2 | ||
: filePathsCount; | ||
const progressBarContainer = createProgressBarContainer(progressBarTotal); | ||
const progressBar = progressBarContainer.create(progressBarTotal, 0); | ||
|
||
const totalSize = { before: 0, after: 0 }; | ||
|
||
const avifConfig = isLossless | ||
? config?.avif?.lossless | ||
: config?.avif?.lossy; | ||
const webpConfig = isLossless | ||
? config?.webp?.lossless | ||
: config?.webp?.lossy; | ||
const webpGifConfig = isLossless | ||
? config?.webpGif?.lossless | ||
: config?.webpGif?.lossy; | ||
|
||
const tasksSimultaneousLimit = pLimit(os.cpus().length); | ||
const tasksPromises = filePaths.reduce((accumulator, filePath) => { | ||
if (shouldConvertToAvif) { | ||
accumulator.push( | ||
tasksSimultaneousLimit( | ||
() => processFile({ | ||
filePath, | ||
config: avifConfig || {}, | ||
progressBarContainer, | ||
progressBar, | ||
totalSize, | ||
isForced, | ||
format: 'AVIF', | ||
processFunction: processAvif, | ||
}), | ||
), | ||
); | ||
} | ||
|
||
if (shouldConvertToWebp) { | ||
accumulator.push( | ||
tasksSimultaneousLimit( | ||
() => processFile({ | ||
filePath, | ||
config: (path.extname(filePath.input).toLowerCase() === '.gif' | ||
? webpGifConfig | ||
: webpConfig) | ||
|| {}, | ||
progressBarContainer, | ||
progressBar, | ||
totalSize, | ||
isForced, | ||
format: 'WebP', | ||
processFunction: processWebp, | ||
}), | ||
), | ||
); | ||
} | ||
|
||
return accumulator; | ||
}, []); | ||
|
||
await Promise.all(tasksPromises); | ||
progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 | ||
progressBarContainer.stop(); | ||
|
||
showTotal(totalSize.before, totalSize.after); | ||
} | ||
|
||
async function processFile({ | ||
filePath, | ||
config, | ||
progressBarContainer, | ||
progressBar, | ||
totalSize, | ||
isForced, | ||
format, | ||
processFunction, | ||
}) { | ||
try { | ||
const { dir, name } = path.parse(filePath.output); | ||
const outputFilePath = path.join(dir, `${name}.${format.toLowerCase()}`); | ||
|
||
const isAccessible = await checkPathAccessibility(outputFilePath); | ||
|
||
if (!isForced && isAccessible) { | ||
logProgressVerbose(getRelativePath(outputFilePath), { | ||
description: `File already exists, '${outputFilePath}'`, | ||
progressBarContainer, | ||
}); | ||
|
||
return; | ||
} | ||
|
||
const fileBuffer = await fs.promises.readFile(filePath.input); | ||
const processedFileBuffer = await processFunction({ fileBuffer, config }); | ||
|
||
await fs.promises.mkdir(path.dirname(outputFilePath), { recursive: true }); | ||
await fs.promises.writeFile(outputFilePath, processedFileBuffer); | ||
|
||
const fileSize = fileBuffer.length; | ||
const processedFileSize = processedFileBuffer.length; | ||
|
||
totalSize.before += fileSize; | ||
totalSize.after += Math.min(fileSize, processedFileSize); | ||
|
||
const ratio = calculateRatio(fileSize, processedFileSize); | ||
const before = formatBytes(fileSize); | ||
const after = formatBytes(processedFileSize); | ||
|
||
logProgress(getRelativePath(filePath.input), { | ||
type: LOG_TYPES.SUCCESS, | ||
description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, | ||
progressBarContainer, | ||
}); | ||
} catch (error) { | ||
if (error.message) { | ||
logProgress(getRelativePath(filePath.input), { | ||
type: LOG_TYPES.ERROR, | ||
description: (error.message || '').trim(), | ||
progressBarContainer, | ||
}); | ||
} else { | ||
progressBarContainer.log(error); | ||
} | ||
} finally { | ||
progressBar.increment(); | ||
} | ||
} | ||
|
||
async function processAvif({ fileBuffer, config }) { | ||
const imageMetadata = await parseImageMetadata(fileBuffer); | ||
checkImageFormat(imageMetadata.format); | ||
|
||
const isAnimated = imageMetadata.pages > 1; | ||
|
||
if (isAnimated) { | ||
throw new Error('Animated AVIF is not supported'); // See: https://github.com/strukturag/libheif/issues/377 | ||
} | ||
|
||
return sharp(fileBuffer) | ||
.rotate() // Rotate image using information from EXIF Orientation tag | ||
.avif(config) | ||
.toBuffer(); | ||
} | ||
|
||
async function processWebp({ fileBuffer, config }) { | ||
const imageMetadata = await parseImageMetadata(fileBuffer); | ||
checkImageFormat(imageMetadata.format); | ||
|
||
if (imageMetadata.format === 'gif') { | ||
return execBuffer({ | ||
bin: gif2webp, | ||
args: [ | ||
...optionsToArguments({ | ||
options: config, | ||
prefix: '-', | ||
}), | ||
execBuffer.input, | ||
'-o', | ||
execBuffer.output, | ||
], | ||
input: fileBuffer, | ||
}); | ||
} | ||
|
||
return sharp(fileBuffer) | ||
.rotate() // Rotate image using information from EXIF Orientation tag | ||
.webp(config) | ||
.toBuffer(); | ||
} | ||
|
||
function checkImageFormat(imageFormat) { | ||
if (!imageFormat) { | ||
throw new Error('Unknown file format'); | ||
} | ||
|
||
if (!SUPPORTED_FILE_TYPES.CONVERT.includes(imageFormat)) { | ||
throw new Error(`Unsupported image format: "${imageFormat}"`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,57 @@ | ||
import { pathToFileURL } from 'node:url'; | ||
|
||
import checkConfigPath from './lib/check-config-path.js'; | ||
import convert from './lib/convert.js'; | ||
import findConfig from './lib/find-config.js'; | ||
import { enableVerbose } from './lib/log.js'; | ||
import optimize from './lib/optimize.js'; | ||
import prepareFilePaths from './lib/prepare-file-paths.js'; | ||
import prepareOutputPath from './lib/prepare-output-path.js'; | ||
|
||
export default async function optimizt({ paths, avif, webp, force, lossless, verbose, output, config }) { | ||
const configFilepath = pathToFileURL(config ? checkConfigPath(config) : findConfig()); | ||
const configData = await import(configFilepath); | ||
|
||
if (verbose) { | ||
enableVerbose(); | ||
import { convert } from './convert.js'; | ||
import { optimize } from './optimize.js'; | ||
|
||
import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; | ||
import { findConfigFilePath } from './lib/find-config-file-path.js'; | ||
import { log } from './lib/log.js'; | ||
import { prepareFilePaths } from './lib/prepare-file-paths.js'; | ||
import { prepareOutputDirectoryPath } from './lib/prepare-output-directory-path.js'; | ||
import { programOptions } from './lib/program-options.js'; | ||
|
||
const MODE_NAME = { | ||
CONVERT: 'convert', | ||
OPTIMIZE: 'optimize', | ||
}; | ||
|
||
export default async function optimizt({ | ||
inputPaths, | ||
outputDirectoryPath, | ||
configFilePath, | ||
}) { | ||
const { | ||
isLossless, | ||
shouldConvertToAvif, | ||
shouldConvertToWebp, | ||
} = programOptions; | ||
|
||
const shouldConvert = shouldConvertToAvif || shouldConvertToWebp; | ||
|
||
const currentMode = shouldConvert | ||
? MODE_NAME.CONVERT | ||
: MODE_NAME.OPTIMIZE; | ||
|
||
const foundConfigFilePath = pathToFileURL(await findConfigFilePath(configFilePath)); | ||
const configData = await import(foundConfigFilePath); | ||
const config = configData.default[currentMode.toLowerCase()]; | ||
|
||
const filePaths = await prepareFilePaths({ | ||
inputPaths, | ||
outputDirectoryPath: await prepareOutputDirectoryPath(outputDirectoryPath), | ||
extensions: SUPPORTED_FILE_TYPES[currentMode.toUpperCase()], | ||
}); | ||
|
||
if (isLossless) { | ||
log('Lossless optimization may take a long time'); | ||
} | ||
|
||
await (avif || webp ? convert({ | ||
paths: prepareFilePaths(paths, ['gif', 'jpeg', 'jpg', 'png']), | ||
lossless, | ||
avif, | ||
webp, | ||
force, | ||
output: prepareOutputPath(output), | ||
config: configData.default.convert, | ||
}) : optimize({ | ||
paths: prepareFilePaths(paths, ['gif', 'jpeg', 'jpg', 'png', 'svg']), | ||
lossless, | ||
output: prepareOutputPath(output), | ||
config: configData.default.optimize, | ||
})); | ||
const processFunction = shouldConvert | ||
? convert | ||
: optimize; | ||
|
||
await processFunction({ | ||
filePaths, | ||
config, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
export default function calcRatio(from, to) { | ||
export function calculateRatio(from, to) { | ||
return Math.round((from - to) / from * 100); | ||
} |
Oops, something went wrong.