pixeli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/bin/pixeli.js +23 -0
- package/commands/aspect.js +69 -0
- package/commands/masonry.js +70 -0
- package/commands/merge.js +12 -0
- package/commands/square.js +71 -0
- package/lib/helpers/loadImages.js +84 -0
- package/lib/helpers/progressBar.js +20 -0
- package/lib/helpers/utils.js +208 -0
- package/lib/helpers/validations.js +246 -0
- package/lib/merges/aspect-merge/index.js +148 -0
- package/lib/merges/masonry-merge/horizontal.js +147 -0
- package/lib/merges/masonry-merge/index.js +57 -0
- package/lib/merges/masonry-merge/vertical.js +146 -0
- package/lib/merges/merge-utils.js +156 -0
- package/lib/merges/square-merge/index.js +141 -0
- package/package.json +34 -0
package/README.md
ADDED
package/bin/pixeli.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import mergeCommand from '../commands/merge.js';
|
|
5
|
+
import { handleError } from '../lib/helpers/utils.js';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
// Define program
|
|
10
|
+
program
|
|
11
|
+
.name('pixeli')
|
|
12
|
+
.description('A lightweight command-line tool for merging multiple images into customizable grid layouts.')
|
|
13
|
+
.version('1.0.0');
|
|
14
|
+
|
|
15
|
+
// Add subcommands
|
|
16
|
+
program.addCommand(mergeCommand);
|
|
17
|
+
|
|
18
|
+
// Parse arguments
|
|
19
|
+
try {
|
|
20
|
+
program.parse();
|
|
21
|
+
} catch (e) {
|
|
22
|
+
handleError(e);
|
|
23
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import {
|
|
4
|
+
addSharedOptions,
|
|
5
|
+
cliConfirm,
|
|
6
|
+
displayInfoMessage,
|
|
7
|
+
displaySuccessMessage,
|
|
8
|
+
displayWarningMessage,
|
|
9
|
+
getValidatedParams,
|
|
10
|
+
handleError,
|
|
11
|
+
writeImage,
|
|
12
|
+
} from '../lib/helpers/utils.js';
|
|
13
|
+
import { validateAspectOptions } from '../lib/helpers/validations.js';
|
|
14
|
+
import { loadImages } from '../lib/helpers/loadImages.js';
|
|
15
|
+
import { aspectMerge } from '../lib/merges/aspect-merge/index.js';
|
|
16
|
+
|
|
17
|
+
const aspectCommand = new Command('aspect');
|
|
18
|
+
|
|
19
|
+
aspectCommand
|
|
20
|
+
.description('Same aspect ratio, but not necessarily 1:1')
|
|
21
|
+
.option('--ar, --aspect-ratio <width/height|number>', 'The aspect ratio of all the images (examples: 16/9, 4:3, 1.777)', null)
|
|
22
|
+
.option('-w, --image-width <px>', 'The width of each image, defaults to the smallest image', null)
|
|
23
|
+
.option('-c, --columns <n>', 'The number of columns', 4)
|
|
24
|
+
.option('--ca, --caption', 'Whether to caption each image', false)
|
|
25
|
+
.option('--cc, --caption-color <hex>', 'Image Caption color', '#000000')
|
|
26
|
+
.option('--mcs, --max-caption-size <pt>', 'The maximum allowed caption size', 100)
|
|
27
|
+
.action(async (files, opts) => {
|
|
28
|
+
await main(files, opts);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const main = async (files, opts) => {
|
|
32
|
+
// Collect and validate parameters
|
|
33
|
+
try {
|
|
34
|
+
const validatedParams = getValidatedParams(files, opts, validateAspectOptions);
|
|
35
|
+
|
|
36
|
+
// Load images, create grid, and write grid on disk
|
|
37
|
+
await generateAndSaveGrid(validatedParams);
|
|
38
|
+
|
|
39
|
+
// Output success message
|
|
40
|
+
} catch (e) {
|
|
41
|
+
handleError(e);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const generateAndSaveGrid = async (validatedParams) => {
|
|
46
|
+
const { files, images, ignoredFiles } = await loadImages(validatedParams);
|
|
47
|
+
|
|
48
|
+
// Display warnings if needed
|
|
49
|
+
if (ignoredFiles.length) {
|
|
50
|
+
displayWarningMessage('\nThese files will be ignored due to unsupported formats:');
|
|
51
|
+
for (const file of ignoredFiles) {
|
|
52
|
+
displayInfoMessage(file);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const confirmation = await cliConfirm('\nAre you sure you want to continue?');
|
|
56
|
+
if (!confirmation) return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const grid = await aspectMerge(files, images, validatedParams);
|
|
60
|
+
const success = await writeImage(grid, validatedParams.output);
|
|
61
|
+
|
|
62
|
+
// Display success message
|
|
63
|
+
if (success) {
|
|
64
|
+
displaySuccessMessage(`\nImage has been created successfully: ${chalk.bold(validatedParams.output)}\n`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
addSharedOptions(aspectCommand);
|
|
69
|
+
export default aspectCommand;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import {
|
|
4
|
+
addSharedOptions,
|
|
5
|
+
cliConfirm,
|
|
6
|
+
displayInfoMessage,
|
|
7
|
+
displaySuccessMessage,
|
|
8
|
+
displayWarningMessage,
|
|
9
|
+
getValidatedParams,
|
|
10
|
+
handleError,
|
|
11
|
+
writeImage,
|
|
12
|
+
} from '../lib/helpers/utils.js';
|
|
13
|
+
import { validateMasonryOptions } from '../lib/helpers/validations.js';
|
|
14
|
+
import { loadImages } from '../lib/helpers/loadImages.js';
|
|
15
|
+
import { masonryMerge } from '../lib/merges/masonry-merge/index.js';
|
|
16
|
+
|
|
17
|
+
const masonryCommand = new Command('masonry');
|
|
18
|
+
|
|
19
|
+
masonryCommand
|
|
20
|
+
.description("Use a ragged-grid layout, preserves images' aspect ratios")
|
|
21
|
+
.option('--rh, --row-height <px>', 'The height of each row, defaults to the smallest image height', null)
|
|
22
|
+
.option('--cw, --column-width <px>', 'The width of each column, defaults to the smallest image width', null)
|
|
23
|
+
.option('--cvw, --canvas-width <px>', 'The width of the canvas', null)
|
|
24
|
+
.option('--cvh, --canvas-height <px>', 'The height of the canvas', null)
|
|
25
|
+
.option('--or, --orientation <horizontal|vertical>', 'The orientation of the masonry layout', 'horizontal')
|
|
26
|
+
.option('--ha, --h-align <left|center|right|justified>', 'Horizontal alignment of the grid (for horizontal orientations)', null)
|
|
27
|
+
.option('--va, --v-align <top|middle|bottom|justified>', 'Vertical alignment of the grid (for vertical orientations)', null)
|
|
28
|
+
.action((files, opts) => {
|
|
29
|
+
main(files, opts);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const main = async (files, opts) => {
|
|
33
|
+
try {
|
|
34
|
+
// Collect and validate parameters
|
|
35
|
+
const validatedParams = getValidatedParams(files, opts, validateMasonryOptions);
|
|
36
|
+
|
|
37
|
+
// Load images, create grid, and write grid on disk
|
|
38
|
+
generateAndSaveGrid(validatedParams);
|
|
39
|
+
|
|
40
|
+
// Output success message
|
|
41
|
+
} catch (e) {
|
|
42
|
+
handleError(e);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const generateAndSaveGrid = async (validatedParams) => {
|
|
47
|
+
const { images, ignoredFiles } = await loadImages(validatedParams);
|
|
48
|
+
|
|
49
|
+
// Display warnings if needed
|
|
50
|
+
if (ignoredFiles.length) {
|
|
51
|
+
displayWarningMessage('\nThese files will be ignored due to unsupported formats:');
|
|
52
|
+
for (const file of ignoredFiles) {
|
|
53
|
+
displayInfoMessage(file);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const confirmation = await cliConfirm('\nAre you sure you want to continue?');
|
|
57
|
+
if (!confirmation) return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const grid = await masonryMerge(images, validatedParams);
|
|
61
|
+
const success = await writeImage(grid, validatedParams.output);
|
|
62
|
+
|
|
63
|
+
// Display success message
|
|
64
|
+
if (success) {
|
|
65
|
+
displaySuccessMessage(`\nImage has been created successfully: ${chalk.bold(validatedParams.output)}\n`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
addSharedOptions(masonryCommand);
|
|
70
|
+
export default masonryCommand;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import squareCommand from './square.js';
|
|
3
|
+
import masonryCommand from './masonry.js';
|
|
4
|
+
import aspectCommand from './aspect.js';
|
|
5
|
+
|
|
6
|
+
const mergeCommand = new Command('merge').description('Merge images into a grid layout.');
|
|
7
|
+
|
|
8
|
+
mergeCommand.addCommand(squareCommand);
|
|
9
|
+
mergeCommand.addCommand(masonryCommand);
|
|
10
|
+
mergeCommand.addCommand(aspectCommand);
|
|
11
|
+
|
|
12
|
+
export default mergeCommand;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import {
|
|
4
|
+
addSharedOptions,
|
|
5
|
+
cliConfirm,
|
|
6
|
+
displayInfoMessage,
|
|
7
|
+
displaySuccessMessage,
|
|
8
|
+
displayWarningMessage,
|
|
9
|
+
getValidatedParams,
|
|
10
|
+
handleError,
|
|
11
|
+
writeImage,
|
|
12
|
+
} from '../lib/helpers/utils.js';
|
|
13
|
+
import { validateSquareOptions } from '../lib/helpers/validations.js';
|
|
14
|
+
import { loadImages } from '../lib/helpers/loadImages.js';
|
|
15
|
+
import { squareMerge } from '../lib/merges/square-merge/index.js';
|
|
16
|
+
|
|
17
|
+
const squareCommand = new Command('square');
|
|
18
|
+
|
|
19
|
+
squareCommand
|
|
20
|
+
.description('Use a uniform grid layout (all images are 1:1)')
|
|
21
|
+
.option('-m, --fit-mode <contain|cover>', 'Determines how to fit the images in their cells', 'contain')
|
|
22
|
+
.option('--is, --image-size <px>', 'The width and height of each image, defaults to the smallest image', null)
|
|
23
|
+
.option('-p, --padding-color <hex|transparent>', 'Image padding color', 'transparent')
|
|
24
|
+
.option('-c, --columns <n>', 'The number of columns', 4)
|
|
25
|
+
.option('--ca, --caption', 'Whether to caption each image', false)
|
|
26
|
+
.option('--cc, --caption-color <hex>', 'Caption color', '#000000')
|
|
27
|
+
.option('--mcs, --max-caption-size <pt>', 'The maximum allowed caption size', 100)
|
|
28
|
+
.action(async (files, opts) => {
|
|
29
|
+
await main(files, opts);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const main = async (files, opts) => {
|
|
33
|
+
// Collect and validate parameters
|
|
34
|
+
try {
|
|
35
|
+
const validatedParams = getValidatedParams(files, opts, validateSquareOptions);
|
|
36
|
+
// console.log(validatedParams);
|
|
37
|
+
|
|
38
|
+
// Load images, create grid, and write grid on disk
|
|
39
|
+
await generateAndSaveGrid(validatedParams);
|
|
40
|
+
|
|
41
|
+
// Output success message
|
|
42
|
+
} catch (e) {
|
|
43
|
+
handleError(e);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const generateAndSaveGrid = async (validatedParams) => {
|
|
48
|
+
const { files, images, ignoredFiles } = await loadImages(validatedParams);
|
|
49
|
+
|
|
50
|
+
// Display warnings if needed
|
|
51
|
+
if (ignoredFiles.length) {
|
|
52
|
+
displayWarningMessage('\nThese files will be ignored due to unsupported formats:');
|
|
53
|
+
for (const file of ignoredFiles) {
|
|
54
|
+
displayInfoMessage(file);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const confirmation = await cliConfirm('\nAre you sure you want to continue?');
|
|
58
|
+
if (!confirmation) return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const grid = await squareMerge(files, images, validatedParams);
|
|
62
|
+
const success = await writeImage(grid, validatedParams.output);
|
|
63
|
+
|
|
64
|
+
// Display success message
|
|
65
|
+
if (success) {
|
|
66
|
+
displaySuccessMessage(`\nImage has been created successfully: ${chalk.bold(validatedParams.output)}\n`);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
addSharedOptions(squareCommand);
|
|
71
|
+
export default squareCommand;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isSupportedInputImage, shuffleTogether } from './utils.js';
|
|
5
|
+
|
|
6
|
+
const MAX_RECURSION_DEPTH = 10;
|
|
7
|
+
|
|
8
|
+
export const loadImages = async ({ files, dir, recursive, shuffle }) => {
|
|
9
|
+
let ignoredFiles = [];
|
|
10
|
+
let filepaths = files;
|
|
11
|
+
let images = [];
|
|
12
|
+
|
|
13
|
+
if (files && files.length) {
|
|
14
|
+
// Load directly from provided file list
|
|
15
|
+
images = await loadFromFiles(files);
|
|
16
|
+
} else {
|
|
17
|
+
// Get all files from directory
|
|
18
|
+
const { skippedFiles, paths } = await getFilesFromDirectory(dir, recursive);
|
|
19
|
+
filepaths = paths;
|
|
20
|
+
ignoredFiles = skippedFiles;
|
|
21
|
+
images = await loadFromFiles(filepaths);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Optional: shuffle filepaths and images together
|
|
25
|
+
if (shuffle) {
|
|
26
|
+
[filepaths, images] = shuffleTogether(filepaths, images);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { images, files: filepaths, ignoredFiles };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const loadFromFiles = async (files) => {
|
|
33
|
+
const images = [];
|
|
34
|
+
|
|
35
|
+
for (const filepath of files) {
|
|
36
|
+
let image;
|
|
37
|
+
|
|
38
|
+
if (filepath.endsWith('.svg')) {
|
|
39
|
+
const svgBuffer = await fs.readFile(filepath);
|
|
40
|
+
image = sharp(svgBuffer);
|
|
41
|
+
} else {
|
|
42
|
+
image = sharp(filepath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
images.push(image);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return images;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getFilesFromDirectory = async (dir, recursive, depth = 0) => {
|
|
52
|
+
// Use to collect warnings
|
|
53
|
+
const skippedFiles = [];
|
|
54
|
+
|
|
55
|
+
// Ensure recursiveness ends at the max recursion depth
|
|
56
|
+
if (depth >= MAX_RECURSION_DEPTH) return [];
|
|
57
|
+
|
|
58
|
+
// Get entries
|
|
59
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
60
|
+
const files = [];
|
|
61
|
+
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const file = path.join(entry.parentPath, entry.name);
|
|
64
|
+
|
|
65
|
+
// If the entry is a valid image file, add it to the list
|
|
66
|
+
if (entry.isFile() && isSupportedInputImage(entry.name)) {
|
|
67
|
+
files.push(file);
|
|
68
|
+
}
|
|
69
|
+
// If it is an invalid file format, add to skipped files
|
|
70
|
+
else if (entry.isFile() && !isSupportedInputImage(entry.name) && entry.name !== '.DS_Store') {
|
|
71
|
+
skippedFiles.push(entry.name);
|
|
72
|
+
}
|
|
73
|
+
// If it's a directory AND the recursive option is true,
|
|
74
|
+
// recursively get all the files
|
|
75
|
+
else if (recursive && entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
76
|
+
const dirpath = path.join(entry.parentPath, entry.name);
|
|
77
|
+
const dirObj = await getFilesFromDirectory(dirpath, recursive, depth + 1);
|
|
78
|
+
files.push(...dirObj.paths);
|
|
79
|
+
skippedFiles.push(...dirObj.skippedFiles);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { paths: files, skippedFiles };
|
|
84
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SingleBar } from 'cli-progress';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
const title = chalk.gray('Creating Image:');
|
|
5
|
+
const bar = chalk.blue('{bar}');
|
|
6
|
+
const percentage = chalk.yellow('{percentage}%');
|
|
7
|
+
const eta = chalk.blue('ETA: ') + chalk.yellow('{eta_formatted}');
|
|
8
|
+
const stage = chalk.gray('{stage}...');
|
|
9
|
+
const divider = chalk.blue('|');
|
|
10
|
+
|
|
11
|
+
export const WRITING_TO_FILE_PERCENTAGE = 0.05;
|
|
12
|
+
|
|
13
|
+
export const progressBar = new SingleBar({
|
|
14
|
+
format: `${title} ${divider}${bar}${divider} ${percentage} ${divider} ${eta} ${divider} ${stage} `,
|
|
15
|
+
barCompleteChar: '\u2588',
|
|
16
|
+
barIncompleteChar: '\u2591',
|
|
17
|
+
stopOnComplete: true,
|
|
18
|
+
barsize: 40,
|
|
19
|
+
etaBuffer: 50,
|
|
20
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { validateSharedOptions } from './validations.js';
|
|
5
|
+
import { progressBar } from './progressBar.js';
|
|
6
|
+
|
|
7
|
+
export const SUPPORTED_INPUT_FORMATS = ['.webp', '.gif', '.jpeg', '.jpg', '.png', '.tiff', '.avif', '.svg'];
|
|
8
|
+
export const SUPPORTED_OUTPUT_FORMATS = ['.webp', '.gif', '.jpeg', '.jpg', '.png', '.tiff', '.avif'];
|
|
9
|
+
|
|
10
|
+
// Message class used for logging
|
|
11
|
+
export class Message {
|
|
12
|
+
constructor(text, type) {
|
|
13
|
+
this.text = text;
|
|
14
|
+
this.type = type;
|
|
15
|
+
|
|
16
|
+
switch (this.type) {
|
|
17
|
+
case 'error':
|
|
18
|
+
this.message = chalk.bold.red('Error: ') + chalk.red(this.text);
|
|
19
|
+
break;
|
|
20
|
+
case 'warning':
|
|
21
|
+
this.message = chalk.yellow(this.text);
|
|
22
|
+
break;
|
|
23
|
+
case 'success':
|
|
24
|
+
this.message = chalk.blue(this.text);
|
|
25
|
+
break;
|
|
26
|
+
case 'neutral':
|
|
27
|
+
this.message = chalk.gray(this.text);
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
this.message = chalk.gray(this.text);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const addSharedOptions = (cmd) => {
|
|
37
|
+
return cmd
|
|
38
|
+
.argument('[files...]', 'Image filepaths to merge (use --dir for directories)')
|
|
39
|
+
.option('-d, --dir <path>', 'Directory of images to merge')
|
|
40
|
+
.option('-r, --recursive', 'Recursively include subdirectories', false)
|
|
41
|
+
.option('--sh, --shuffle', 'Shuffle up images to randomize order in the grid', false)
|
|
42
|
+
.option('-g, --gap <px>', 'Gap between images', 50)
|
|
43
|
+
.option('--bg, --canvas-color <hex>', 'Background color for canvas', '#ffffff')
|
|
44
|
+
.option('-o, --output <file>', 'Output file path', './pixeli.png');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const isValidHexadecimal = (str) => {
|
|
48
|
+
const hexRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
49
|
+
return hexRegex.test(str);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const parseAspectRatio = (input) => {
|
|
53
|
+
// return ratio straight away if its just a number
|
|
54
|
+
const ratio = Number(input);
|
|
55
|
+
if (ratio) {
|
|
56
|
+
return ratio;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ratioRegex = /^\s*(\d+)\s*(\/|:|x)\s*(\d+)\s*$/i;
|
|
60
|
+
const match = input.match(ratioRegex);
|
|
61
|
+
|
|
62
|
+
// not parsable
|
|
63
|
+
if (!match) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const width = parseInt(match[1], 10);
|
|
68
|
+
const height = parseInt(match[3], 10);
|
|
69
|
+
|
|
70
|
+
return width / height;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const handleError = (error) => {
|
|
74
|
+
const m = new Message(error.message, 'error');
|
|
75
|
+
console.log(m.message);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const displayInfoMessage = (message) => {
|
|
79
|
+
const m = new Message(message, 'neutral');
|
|
80
|
+
console.log(m.message);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const displayWarningMessage = (message) => {
|
|
84
|
+
const m = new Message(message, 'warning');
|
|
85
|
+
console.log(m.message);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const displaySuccessMessage = (message) => {
|
|
89
|
+
const m = new Message(message, 'success');
|
|
90
|
+
console.log(m.message);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const cliConfirm = (message) => {
|
|
94
|
+
const rl = readline.createInterface({
|
|
95
|
+
input: process.stdin,
|
|
96
|
+
output: process.stdout,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
rl.question(chalk.yellow(`${message} (Y/n) `), (value) => {
|
|
101
|
+
const cleanedValue = value.toLowerCase().trim();
|
|
102
|
+
if (cleanedValue === 'y' || !cleanedValue.length) resolve(true);
|
|
103
|
+
else resolve(false);
|
|
104
|
+
|
|
105
|
+
console.log();
|
|
106
|
+
rl.close();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const isSupportedInputImage = (filename) => {
|
|
112
|
+
for (const supportedFormat of SUPPORTED_INPUT_FORMATS) {
|
|
113
|
+
if (filename.endsWith(supportedFormat)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const isSupportedOutputImage = (filename) => {
|
|
121
|
+
for (const supportedFormat of SUPPORTED_OUTPUT_FORMATS) {
|
|
122
|
+
if (filename.endsWith(supportedFormat)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const writeImage = async (image, output) => {
|
|
130
|
+
// Define file size limits
|
|
131
|
+
const LIMITS = {
|
|
132
|
+
png: 2_147_483_647,
|
|
133
|
+
jpg: 65_535,
|
|
134
|
+
jpeg: 65_535,
|
|
135
|
+
avif: 65_535,
|
|
136
|
+
webp: 16_383,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Update progress bar stage
|
|
140
|
+
progressBar.update({ stage: 'Writing to file' });
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Get image width and height
|
|
144
|
+
const { width, height } = await image.metadata();
|
|
145
|
+
const format = path.extname(output).replaceAll('.', '');
|
|
146
|
+
|
|
147
|
+
// Ensure image can be encoded in the respective format
|
|
148
|
+
const formatLimit = LIMITS[format];
|
|
149
|
+
if (width > formatLimit || height > formatLimit) {
|
|
150
|
+
throw new Error(`image is too large for ${format} format.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Write to file
|
|
154
|
+
await image.toFile(output);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// Complete the progress bar
|
|
157
|
+
progressBar.update(progressBar.getTotal());
|
|
158
|
+
|
|
159
|
+
// Handle any errors
|
|
160
|
+
const m = new Message('Failed to write image on disk: ' + e.message, 'error');
|
|
161
|
+
console.log('\n' + m.message);
|
|
162
|
+
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Complete the progress bar
|
|
167
|
+
progressBar.update(progressBar.getTotal());
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const getValidatedParams = (files, opts, validationFunc) => {
|
|
173
|
+
const params = { files, ...opts };
|
|
174
|
+
const sharedOptions = validateSharedOptions(params);
|
|
175
|
+
const commandOptions = validationFunc(sharedOptions, params);
|
|
176
|
+
return { ...sharedOptions, ...commandOptions };
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const shuffleArray = (array) => {
|
|
180
|
+
let currentIndex = array.length;
|
|
181
|
+
let randomIndex;
|
|
182
|
+
|
|
183
|
+
// While there remain elements to shuffle.
|
|
184
|
+
while (currentIndex !== 0) {
|
|
185
|
+
// Pick a remaining element.
|
|
186
|
+
randomIndex = Math.floor(Math.random() * currentIndex);
|
|
187
|
+
currentIndex--;
|
|
188
|
+
|
|
189
|
+
// And swap it with the current element.
|
|
190
|
+
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return array;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const shuffleTogether = (a, b) => {
|
|
197
|
+
// 1. Build array of indices
|
|
198
|
+
const indices = [...a.keys()];
|
|
199
|
+
|
|
200
|
+
// 2. Shuffle the indices using your Fisher-Yates
|
|
201
|
+
shuffleArray(indices);
|
|
202
|
+
|
|
203
|
+
// 3. Apply same permutation to both arrays
|
|
204
|
+
const aShuffled = indices.map((i) => a[i]);
|
|
205
|
+
const bShuffled = indices.map((i) => b[i]);
|
|
206
|
+
|
|
207
|
+
return [aShuffled, bShuffled];
|
|
208
|
+
};
|