Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring #10

Merged
merged 45 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
00c90f9
Install fdir
343dev Sep 22, 2024
b20869e
Replace fast-glob with fdir
343dev Sep 22, 2024
2d5da0c
Uninstall fast-glob
343dev Sep 22, 2024
0e6dcde
Improve index.js
343dev Sep 23, 2024
29cfadb
Rename prepareFilePaths to prepareInputFilePaths
343dev Sep 24, 2024
f93b8cb
Rename prepareOutputPath to prepareOutputDirectoryPath
343dev Sep 24, 2024
28e4e2c
Rename convert & optimize functions arguments
343dev Sep 24, 2024
375c689
Improve optimize function
343dev Sep 24, 2024
ff954ce
Add lib/constants.js
343dev Sep 25, 2024
74ce5c7
Improve convert function
343dev Sep 25, 2024
063a285
Move lossless optimization notice to index.js
343dev Sep 25, 2024
b5576c9
Add notice about Animated AVIF is not supported
343dev Sep 25, 2024
0dc02cc
Change image optimization workflow
343dev Sep 25, 2024
1bb84e4
Change image convert workflow
343dev Sep 25, 2024
b02519b
Fix progress bar in convert mode
343dev Sep 26, 2024
09bdb8f
Add interactive progress log
343dev Sep 26, 2024
126325e
Change "File already exists" message type from "error" to "info" and …
343dev Sep 27, 2024
0be51df
Rename calcRatio to calculateRatio
343dev Sep 28, 2024
fba685e
Remove lib/check-config-path.js
343dev Sep 28, 2024
1e5f4c3
Rename checkFileExists in checkPathAccessibility
343dev Sep 28, 2024
c27a3e8
Remove default export from colorize
343dev Sep 28, 2024
609727c
Remove default export from createProgressBarContainer
343dev Sep 28, 2024
e7b61de
Create DEFAULT_CONFIG_FILENAME constant
343dev Sep 28, 2024
bac2c12
Improve findConfig
343dev Sep 28, 2024
4106ba1
Improve formatBytes
343dev Sep 28, 2024
49b6b36
Remove default export from getPlural
343dev Sep 28, 2024
d07ef4a
Improve log
343dev Sep 28, 2024
6be4f6d
Improve optionsToArguments
343dev Sep 29, 2024
3d992ad
Remove default export from parseImageMetadata
343dev Sep 29, 2024
8f67772
Improve prepareInputFilePaths
343dev Sep 29, 2024
e66ef40
Improve prepareOutputDirectoryPath
343dev Sep 29, 2024
1113206
Add programOptions helper
343dev Sep 30, 2024
2d3ce92
Improve showTotal
343dev Sep 30, 2024
b56e899
Fix output to custom directory
343dev Oct 1, 2024
081a879
Improve findConfig and rename to findConfigFilePath
343dev Oct 1, 2024
32048c6
Remove prepareWriteFilePath
343dev Oct 1, 2024
59a2dba
Remove default export from convert
343dev Oct 1, 2024
826496d
Remove default export from optimize
343dev Oct 1, 2024
f60649b
Move convert.js to project root
343dev Oct 1, 2024
c951ef4
Move optimize.js to project root
343dev Oct 1, 2024
2ed50e4
Bump fdir from 6.3.0 to 6.4.0
343dev Oct 2, 2024
cf23f5e
Bump lint-staged from 15.2.8 to 15.2.10
343dev Oct 2, 2024
05ab71e
Update MIGRATION.md
343dev Oct 3, 2024
c239efb
Update CHANGELOG.md
343dev Oct 3, 2024
dfb1bd9
9.0.0
343dev Oct 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
!lib/
!.optimiztrc.cjs
!cli.js
!convert.js
!index.js
!LICENSE
!optimize.js
!package*.json
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 9.0.0 (2024-10-03)

Changes:

- Improved image processing workflow. Check the [migration guide](./MIGRATION.md) for details.
- Added an interactive log for image processing.
- Changed the “File already exists” message from “error” to “info,” now only shown in verbose mode.
- Added a notice that animated AVIF is not supported (unfortunately).
- Fixed the progress indicator in conversion mode. It now correctly shows the total number of items.
- Fixed output to the user directory. The original folder structure is now preserved (i hope so!).

## 8.0.0 (2024-08-05)

❤️ Thank you for using Optimizt. If you have any suggestions or feedback, please don't hesitate to [open an issue](https://github.com/343dev/optimizt/issues).
Expand Down
11 changes: 11 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Migration

## 8.0.1 → 9.0.0

The main change in the new version is how Optimizt handles file processing. Before, the result was stored in memory until all files were processed. This could cause the app to crash if it ran out of memory, leading to a loss of optimization results. Now, each file is processed one by one, and the result is saved to disk immediately. Logging events also happen in real-time, instead of waiting until all files are done.

Previously, if you tried to create an AVIF or WebP file that already existed, an error message would appear in the log. Now, it shows an informational message, but only if you use the --verbose option.

To handle AVIF files, Optimizt uses the sharp module, which uses the libheif library. However, libheif doesn't support converting to animated AVIF (AVIS). In earlier versions, Optimizt would quietly convert animated GIFs into static AVIF. But from this version, an error will show up if you try to do this.

The logic for creating file structures when using the --output option has also changed. Now, the original file structure will be recreated inside the output folder, starting from the folder passed to the app. Who knows how this worked before, but hopefully, it works better now!


## 7.0.2 → 8.0.0

Optimizt now checks for the EXIF Orientation tag before processing images and rotates the resulting image accordingly.
Expand Down
16 changes: 14 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url';
import { program } from 'commander';

import optimizt from './index.js';
import { setProgramOptions } from './lib/program-options.js';

const dirname = path.dirname(fileURLToPath(import.meta.url));
const packageJson = JSON.parse(await fs.readFile(path.join(dirname, 'package.json')));
Expand All @@ -29,9 +30,20 @@ program
if (program.args.length === 0) {
program.help();
} else {
const { avif, webp, force, lossless, verbose, config, output } = program.opts();

setProgramOptions({
shouldConvertToAvif: Boolean(avif),
shouldConvertToWebp: Boolean(webp),
isForced: Boolean(force),
isLossless: Boolean(lossless),
isVerbose: Boolean(verbose),
});

optimizt({
paths: program.args,
...program.opts(),
inputPaths: program.args,
outputDirectoryPath: output,
configFilePath: config,
});
}

Expand Down
222 changes: 222 additions & 0 deletions convert.js
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}"`);
}
}
80 changes: 52 additions & 28 deletions index.js
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,
});
}
2 changes: 1 addition & 1 deletion lib/calc-ratio.js → lib/calculate-ratio.js
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);
}
Loading
Loading