pixeli 0.1.4 → 0.1.6
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 +4 -0
- package/bin/pixeli.js +5 -2
- package/commands/{grid.js → merge/grid.js} +6 -7
- package/commands/merge/helpers/utils.js +20 -0
- package/{lib → commands/merge}/helpers/validations.js +26 -3
- package/commands/{masonry.js → merge/masonry.js} +6 -7
- package/lib/helpers/loadImages.js +2 -2
- package/lib/helpers/utils.js +19 -19
- package/lib/merges/grid-merge/index.js +6 -3
- package/lib/merges/masonry-merge/horizontal.js +14 -5
- package/lib/merges/masonry-merge/index.js +2 -2
- package/lib/merges/masonry-merge/vertical.js +14 -5
- package/lib/merges/merge-utils.js +24 -4
- package/package.json +4 -9
- /package/commands/{merge.js → merge/index.js} +0 -0
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ The following options and flags are shared for all of the subcommands under the
|
|
|
92
92
|
| `-r`, `--recursive` | `false` | Include **images in all subdirectories** of the specified directory recursively. |
|
|
93
93
|
| `--sh`, `--shuffle` | `false` | **Randomize the order** of images before merging. Useful for creating visually varied grids or collages. |
|
|
94
94
|
| `-g`, `--gap <px>` | `50` | **Spacing (in pixels) between images** in the layout. Applies to both horizontal and vertical gaps. |
|
|
95
|
+
| `--cr`, `--corner-radius <px>` | `0` | How much to **round the corners** of each image in pixels. |
|
|
95
96
|
| `--bg`, `--canvas-color <hex>` | `#ffffff` | Sets the **background color of the canvas**. Accepts HEX values (e.g., `#000000` for black). |
|
|
96
97
|
| `-o`, `--output <file>` | `./pixeli.png` | Path for the **merged output image**. The format is inferred from the file extension (`.png`, `.jpg`, `.webp`, etc.). |
|
|
97
98
|
|
|
@@ -126,3 +127,6 @@ The masonry mode preserves each image’s natural shape, creating an organic bri
|
|
|
126
127
|
|
|
127
128
|
## License
|
|
128
129
|
This project is licensed under the [MIT License](./LICENSE).
|
|
130
|
+
|
|
131
|
+
## Other
|
|
132
|
+
This project was submitted to [Hack Club](https://hackclub.com/), a group consisting of over 100,000 teen hackers from around the world who work on cool projects and get to participate in awesome programs like [Midnight](https://midnight.hackclub.com).
|
package/bin/pixeli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import mergeCommand from '../commands/merge.js';
|
|
5
|
-
import { handleError } from '../lib/helpers/utils.js';
|
|
4
|
+
import mergeCommand from '../commands/merge/index.js';
|
|
5
|
+
import { configureCommandErrors, handleError } from '../lib/helpers/utils.js';
|
|
6
6
|
|
|
7
7
|
const program = new Command();
|
|
8
8
|
|
|
@@ -15,6 +15,9 @@ program
|
|
|
15
15
|
// Add subcommands
|
|
16
16
|
program.addCommand(mergeCommand);
|
|
17
17
|
|
|
18
|
+
// Configure errors for all subcommands
|
|
19
|
+
configureCommandErrors(program);
|
|
20
|
+
|
|
18
21
|
// Parse arguments
|
|
19
22
|
try {
|
|
20
23
|
program.parse();
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import {
|
|
4
|
-
addSharedOptions,
|
|
5
4
|
cliConfirm,
|
|
6
5
|
displayInfoMessage,
|
|
7
6
|
displaySuccessMessage,
|
|
8
7
|
displayWarningMessage,
|
|
9
|
-
getValidatedParams,
|
|
10
8
|
handleError,
|
|
11
9
|
writeImage,
|
|
12
|
-
} from '
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
10
|
+
} from '../../lib/helpers/utils.js';
|
|
11
|
+
import { addSharedOptions, getValidatedParams } from './helpers/utils.js';
|
|
12
|
+
import { validateGridOptions } from './helpers/validations.js';
|
|
13
|
+
import { loadImages } from '../../lib/helpers/loadImages.js';
|
|
14
|
+
import { gridMerge } from '../../lib/merges/grid-merge/index.js';
|
|
16
15
|
|
|
17
16
|
const gridCommand = new Command('grid');
|
|
18
17
|
|
|
@@ -31,7 +30,7 @@ gridCommand
|
|
|
31
30
|
const main = async (files, opts) => {
|
|
32
31
|
// Collect and validate parameters
|
|
33
32
|
try {
|
|
34
|
-
const validatedParams = getValidatedParams(files, opts, validateGridOptions);
|
|
33
|
+
const validatedParams = await getValidatedParams(files, opts, validateGridOptions);
|
|
35
34
|
|
|
36
35
|
// Load images, create grid, and write grid on disk
|
|
37
36
|
await generateAndSaveGrid(validatedParams);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { validateSharedOptions } from './validations.js';
|
|
2
|
+
|
|
3
|
+
export const addSharedOptions = (cmd) => {
|
|
4
|
+
return cmd
|
|
5
|
+
.argument('[files...]', 'Image filepaths to merge (use --dir for directories)')
|
|
6
|
+
.option('-d, --dir <path>', 'Directory of images to merge')
|
|
7
|
+
.option('-r, --recursive', 'Recursively include subdirectories', false)
|
|
8
|
+
.option('--sh, --shuffle', 'Shuffle up images to randomize order in the grid', false)
|
|
9
|
+
.option('--cr, --corner-radius <px>', 'How much to round the corners of each image', 0)
|
|
10
|
+
.option('-g, --gap <px>', 'Gap between images', 50)
|
|
11
|
+
.option('--bg, --canvas-color <hex|transparent>', 'Background color for canvas', '#ffffff')
|
|
12
|
+
.option('-o, --output <file>', 'Output file path', './pixeli.png');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getValidatedParams = async (files, opts, validationFunc) => {
|
|
16
|
+
const params = { files, ...opts };
|
|
17
|
+
const sharedOptions = await validateSharedOptions(params);
|
|
18
|
+
const commandOptions = validationFunc(sharedOptions, params);
|
|
19
|
+
return { ...sharedOptions, ...commandOptions };
|
|
20
|
+
};
|
|
@@ -1,24 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
displayWarningMessage,
|
|
5
|
+
handleError,
|
|
3
6
|
isSupportedOutputImage,
|
|
4
7
|
isValidHexadecimal,
|
|
5
8
|
parseAspectRatio,
|
|
6
9
|
SUPPORTED_OUTPUT_FORMATS,
|
|
7
|
-
} from '
|
|
10
|
+
} from '../../../lib/helpers/utils.js';
|
|
8
11
|
|
|
9
|
-
export const validateSharedOptions = (sharedOptions) => {
|
|
12
|
+
export const validateSharedOptions = async (sharedOptions) => {
|
|
10
13
|
// Extract params
|
|
11
|
-
const { files, dir, recursive, shuffle, gap, canvasColor, output } = sharedOptions;
|
|
14
|
+
const { files, dir, recursive, shuffle, cornerRadius, gap, canvasColor, output } = sharedOptions;
|
|
12
15
|
|
|
13
16
|
// Conduct validations
|
|
14
17
|
if ((!files || !files.length) && !dir) {
|
|
15
18
|
throw new Error('You must specify either [files...] or --dir.');
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
// Ensure dir is a valid dir path
|
|
22
|
+
if (dir.length) {
|
|
23
|
+
let stats;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
stats = await fs.stat(dir);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
throw new Error('Path does not exist.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!stats.isDirectory()) {
|
|
32
|
+
throw new Error('Path is not a directory.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
if (isNaN(gap) || !Number.isInteger(Number(gap)) || gap < 0) {
|
|
19
37
|
throw new Error('--gap must be a positive integer.');
|
|
20
38
|
}
|
|
21
39
|
|
|
40
|
+
if (isNaN(cornerRadius) || !Number.isInteger(Number(cornerRadius)) || cornerRadius < 0) {
|
|
41
|
+
throw new Error('--corner-radius must be a positive integer.');
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
if (canvasColor !== 'transparent' && !isValidHexadecimal(canvasColor)) {
|
|
23
45
|
throw new Error('--canvas-color must be a valid hexadecimal value.');
|
|
24
46
|
}
|
|
@@ -32,6 +54,7 @@ export const validateSharedOptions = (sharedOptions) => {
|
|
|
32
54
|
dir,
|
|
33
55
|
recursive,
|
|
34
56
|
shuffle,
|
|
57
|
+
cornerRadius: Number(cornerRadius),
|
|
35
58
|
gap: Number(gap),
|
|
36
59
|
canvasColor: canvasColor === 'transparent' ? { r: 0, g: 0, b: 0, alpha: 0 } : canvasColor,
|
|
37
60
|
output,
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import {
|
|
4
|
-
addSharedOptions,
|
|
5
4
|
cliConfirm,
|
|
6
5
|
displayInfoMessage,
|
|
7
6
|
displaySuccessMessage,
|
|
8
7
|
displayWarningMessage,
|
|
9
|
-
getValidatedParams,
|
|
10
8
|
handleError,
|
|
11
9
|
writeImage,
|
|
12
|
-
} from '
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
10
|
+
} from '../../lib/helpers/utils.js';
|
|
11
|
+
import { addSharedOptions, getValidatedParams } from './helpers/utils.js';
|
|
12
|
+
import { validateMasonryOptions } from './helpers/validations.js';
|
|
13
|
+
import { loadImages } from '../../lib/helpers/loadImages.js';
|
|
14
|
+
import { masonryMerge } from '../../lib/merges/masonry-merge/index.js';
|
|
16
15
|
|
|
17
16
|
const masonryCommand = new Command('masonry');
|
|
18
17
|
|
|
@@ -32,7 +31,7 @@ masonryCommand
|
|
|
32
31
|
const main = async (files, opts) => {
|
|
33
32
|
try {
|
|
34
33
|
// Collect and validate parameters
|
|
35
|
-
const validatedParams = getValidatedParams(files, opts, validateMasonryOptions);
|
|
34
|
+
const validatedParams = await getValidatedParams(files, opts, validateMasonryOptions);
|
|
36
35
|
|
|
37
36
|
// Load images, create grid, and write grid on disk
|
|
38
37
|
generateAndSaveGrid(validatedParams);
|
|
@@ -53,7 +53,7 @@ const getFilesFromDirectory = async (dir, recursive, depth = 0) => {
|
|
|
53
53
|
const skippedFiles = [];
|
|
54
54
|
|
|
55
55
|
// Ensure recursiveness ends at the max recursion depth
|
|
56
|
-
if (depth >= MAX_RECURSION_DEPTH) return [];
|
|
56
|
+
if (depth >= MAX_RECURSION_DEPTH) return { paths: [], skippedFiles: [] };
|
|
57
57
|
|
|
58
58
|
// Get entries
|
|
59
59
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -75,7 +75,7 @@ const getFilesFromDirectory = async (dir, recursive, depth = 0) => {
|
|
|
75
75
|
else if (recursive && entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
76
76
|
const dirpath = path.join(entry.parentPath, entry.name);
|
|
77
77
|
const dirObj = await getFilesFromDirectory(dirpath, recursive, depth + 1);
|
|
78
|
-
files.push(...dirObj
|
|
78
|
+
files.push(...dirObj?.paths);
|
|
79
79
|
skippedFiles.push(...dirObj.skippedFiles);
|
|
80
80
|
}
|
|
81
81
|
}
|
package/lib/helpers/utils.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import readline from 'node:readline';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { validateSharedOptions } from './validations.js';
|
|
5
4
|
import { progressBar } from './progressBar.js';
|
|
6
5
|
|
|
7
6
|
export const SUPPORTED_INPUT_FORMATS = ['.webp', '.gif', '.jpeg', '.jpg', '.png', '.tiff', '.avif', '.svg'];
|
|
@@ -33,17 +32,6 @@ export class Message {
|
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
|
|
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|transparent>', 'Background color for canvas', '#ffffff')
|
|
44
|
-
.option('-o, --output <file>', 'Output file path', './pixeli.png');
|
|
45
|
-
};
|
|
46
|
-
|
|
47
35
|
export const isValidHexadecimal = (str) => {
|
|
48
36
|
const hexRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
49
37
|
return hexRegex.test(str);
|
|
@@ -75,6 +63,25 @@ export const handleError = (error) => {
|
|
|
75
63
|
console.log(m.message);
|
|
76
64
|
};
|
|
77
65
|
|
|
66
|
+
export const configureCommandErrors = (cmd) => {
|
|
67
|
+
// Configure error message for the current command
|
|
68
|
+
cmd.configureOutput({
|
|
69
|
+
writeErr: (str) => {
|
|
70
|
+
if (str.includes('error:')) {
|
|
71
|
+
const err = new Message(str.replace('error: ', ''), 'error');
|
|
72
|
+
return process.stderr.write(err.message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.stderr.write(str);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Recursively configure error messages for all subcommands
|
|
80
|
+
for (const subCmd of cmd.commands) {
|
|
81
|
+
configureCommandErrors(subCmd);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
78
85
|
export const displayInfoMessage = (message) => {
|
|
79
86
|
const m = new Message(message, 'neutral');
|
|
80
87
|
console.log(m.message);
|
|
@@ -169,13 +176,6 @@ export const writeImage = async (image, output) => {
|
|
|
169
176
|
return true;
|
|
170
177
|
};
|
|
171
178
|
|
|
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
179
|
export const shuffleArray = (array) => {
|
|
180
180
|
let currentIndex = array.length;
|
|
181
181
|
let randomIndex;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import sharp from 'sharp';
|
|
3
|
-
import { getSmallestImageDimensions, getFontSize, createSvgTextBuffer } from '../merge-utils.js';
|
|
3
|
+
import { getSmallestImageDimensions, getFontSize, createSvgTextBuffer, roundImages } from '../merge-utils.js';
|
|
4
4
|
import { progressBar, WRITING_TO_FILE_PERCENTAGE } from '../../helpers/progressBar.js';
|
|
5
5
|
|
|
6
6
|
export const gridMerge = async (files, images, validatedParams) => {
|
|
7
7
|
// Destructure params
|
|
8
|
-
const { aspectRatio, imageWidth, columns, gap, canvasColor, caption, captionColor, maxCaptionSize } = validatedParams;
|
|
8
|
+
const { aspectRatio, imageWidth, columns, gap, cornerRadius, canvasColor, caption, captionColor, maxCaptionSize } = validatedParams;
|
|
9
9
|
|
|
10
10
|
// Calculate width if needed, and height from aspect ratio
|
|
11
11
|
const width = imageWidth || (await getSmallestImageDimensions(images)).smallestWidth;
|
|
@@ -20,6 +20,9 @@ export const gridMerge = async (files, images, validatedParams) => {
|
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
// Round images if needed
|
|
24
|
+
const roundedImages = await roundImages(resizedImages, { width, height, cornerRadius });
|
|
25
|
+
|
|
23
26
|
// Get filenames if needed
|
|
24
27
|
let filenames = null;
|
|
25
28
|
if (caption) {
|
|
@@ -28,7 +31,7 @@ export const gridMerge = async (files, images, validatedParams) => {
|
|
|
28
31
|
|
|
29
32
|
// Lay images in a grid
|
|
30
33
|
const gridParams = {
|
|
31
|
-
images:
|
|
34
|
+
images: roundedImages,
|
|
32
35
|
width,
|
|
33
36
|
height,
|
|
34
37
|
columns,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
|
-
import { scaleImages } from '../merge-utils.js';
|
|
2
|
+
import { roundImages, scaleImages } from '../merge-utils.js';
|
|
3
3
|
import { progressBar, WRITING_TO_FILE_PERCENTAGE } from '../../helpers/progressBar.js';
|
|
4
4
|
|
|
5
5
|
export const buildHorizontalMasonry = async (images, params) => {
|
|
6
|
-
const { gap, canvasColor, canvasWidth, rowHeight, hAlign } = params;
|
|
6
|
+
const { gap, canvasColor, cornerRadius, canvasWidth, rowHeight, hAlign } = params;
|
|
7
7
|
|
|
8
8
|
// Use 5% of images.length for writing to file
|
|
9
9
|
const fileWriteAmount = Math.ceil(images.length * WRITING_TO_FILE_PERCENTAGE);
|
|
@@ -19,10 +19,10 @@ export const buildHorizontalMasonry = async (images, params) => {
|
|
|
19
19
|
const canvasHeight = rows.length * rowHeight + (rows.length + 1) * gap;
|
|
20
20
|
|
|
21
21
|
// Create and return grid of images
|
|
22
|
-
return await createMasonryLayout(rows, rowHeight, canvasWidth, canvasHeight, canvasColor, gap, hAlign);
|
|
22
|
+
return await createMasonryLayout(rows, rowHeight, canvasWidth, canvasHeight, canvasColor, cornerRadius, gap, hAlign);
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const createMasonryLayout = async (rows, rowHeight, canvasWidth, canvasHeight, canvasColor, gap, hAlign) => {
|
|
25
|
+
const createMasonryLayout = async (rows, rowHeight, canvasWidth, canvasHeight, canvasColor, cornerRadius, gap, hAlign) => {
|
|
26
26
|
const canvas = sharp({
|
|
27
27
|
limitInputPixels: false,
|
|
28
28
|
create: {
|
|
@@ -66,8 +66,17 @@ const createMasonryLayout = async (rows, rowHeight, canvasWidth, canvasHeight, c
|
|
|
66
66
|
finalizedMeta = await finalizedImage.metadata();
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Round the finalized image
|
|
70
|
+
const roundingOptions = {
|
|
71
|
+
width: finalizedMeta.width,
|
|
72
|
+
height: finalizedMeta.height,
|
|
73
|
+
cornerRadius,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const roundedImage = (await roundImages([finalizedImage], roundingOptions))[0];
|
|
77
|
+
|
|
69
78
|
composites.push({
|
|
70
|
-
input: await
|
|
79
|
+
input: await roundedImage.toBuffer(),
|
|
71
80
|
left: x,
|
|
72
81
|
top: y,
|
|
73
82
|
});
|
|
@@ -27,10 +27,10 @@ export const masonryMerge = async (images, opts) => {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const getFlowSpecificParams = async (images, currentParams) => {
|
|
30
|
-
const { flow, gap, canvasColor } = currentParams;
|
|
30
|
+
const { flow, gap, canvasColor, cornerRadius } = currentParams;
|
|
31
31
|
const config = FLOW_DEFAULTS[flow];
|
|
32
32
|
|
|
33
|
-
const output = { gap, canvasColor };
|
|
33
|
+
const output = { gap, canvasColor, cornerRadius };
|
|
34
34
|
|
|
35
35
|
for (const key of config.needed) {
|
|
36
36
|
if (currentParams[key] != null) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
|
-
import { scaleImages } from '../merge-utils.js';
|
|
2
|
+
import { roundImages, scaleImages } from '../merge-utils.js';
|
|
3
3
|
import { progressBar, WRITING_TO_FILE_PERCENTAGE } from '../../helpers/progressBar.js';
|
|
4
4
|
|
|
5
5
|
export const buildVerticalMasonry = async (images, params) => {
|
|
6
|
-
const { gap, canvasColor, canvasHeight, columnWidth, vAlign } = params;
|
|
6
|
+
const { gap, canvasColor, cornerRadius, canvasHeight, columnWidth, vAlign } = params;
|
|
7
7
|
|
|
8
8
|
// Use 5% of images.length for writing to file
|
|
9
9
|
const fileWriteAmount = Math.ceil(images.length * WRITING_TO_FILE_PERCENTAGE);
|
|
@@ -19,10 +19,10 @@ export const buildVerticalMasonry = async (images, params) => {
|
|
|
19
19
|
const canvasWidth = columns.length * columnWidth + (columns.length + 1) * gap;
|
|
20
20
|
|
|
21
21
|
// Create and return grid of images
|
|
22
|
-
return await createMasonryLayout(columns, columnWidth, canvasWidth, canvasHeight, canvasColor, gap, vAlign);
|
|
22
|
+
return await createMasonryLayout(columns, columnWidth, canvasWidth, canvasHeight, canvasColor, cornerRadius, gap, vAlign);
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const createMasonryLayout = async (cols, columnWidth, canvasWidth, canvasHeight, canvasColor, gap, vAlign) => {
|
|
25
|
+
const createMasonryLayout = async (cols, columnWidth, canvasWidth, canvasHeight, canvasColor, cornerRadius, gap, vAlign) => {
|
|
26
26
|
const composites = [];
|
|
27
27
|
|
|
28
28
|
const canvas = sharp({
|
|
@@ -61,8 +61,17 @@ const createMasonryLayout = async (cols, columnWidth, canvasWidth, canvasHeight,
|
|
|
61
61
|
finalizedMeta = await finalizedImage.metadata();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// Round the finalized image
|
|
65
|
+
const roundingOptions = {
|
|
66
|
+
width: finalizedMeta.width,
|
|
67
|
+
height: finalizedMeta.height,
|
|
68
|
+
cornerRadius,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const roundedImage = (await roundImages([finalizedImage], roundingOptions))[0];
|
|
72
|
+
|
|
64
73
|
composites.push({
|
|
65
|
-
input: await
|
|
74
|
+
input: await roundedImage.toBuffer(),
|
|
66
75
|
left: x,
|
|
67
76
|
top: y,
|
|
68
77
|
});
|
|
@@ -33,7 +33,10 @@ export const scaleImages = async (images, { width = null, height = null }) => {
|
|
|
33
33
|
|
|
34
34
|
let targetWidth, targetHeight;
|
|
35
35
|
|
|
36
|
-
if (width) {
|
|
36
|
+
if (width && height) {
|
|
37
|
+
targetWidth = width;
|
|
38
|
+
targetHeight = height;
|
|
39
|
+
} else if (width) {
|
|
37
40
|
const f = width / meta.width;
|
|
38
41
|
targetWidth = width;
|
|
39
42
|
targetHeight = Math.floor(meta.height * f);
|
|
@@ -150,7 +153,24 @@ const escapeXML = (str) => {
|
|
|
150
153
|
);
|
|
151
154
|
};
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
export const roundImages = async (images, { width, height, cornerRadius }) => {
|
|
157
|
+
// Skip if the cornerRadius = zero
|
|
158
|
+
if (!cornerRadius) return images;
|
|
155
159
|
|
|
156
|
-
|
|
160
|
+
// Round images respectively
|
|
161
|
+
return await Promise.all(
|
|
162
|
+
images.map(async (image) => {
|
|
163
|
+
const mask = Buffer.from(`
|
|
164
|
+
<svg width="${width}" height="${height}">
|
|
165
|
+
<rect x="0" y="0" width="${width}" height="${height}" rx="${cornerRadius}" ry="${cornerRadius}" />
|
|
166
|
+
</svg>
|
|
167
|
+
`);
|
|
168
|
+
|
|
169
|
+
const buff = await image
|
|
170
|
+
.composite([{ input: mask, blend: 'dest-in' }])
|
|
171
|
+
.png()
|
|
172
|
+
.toBuffer();
|
|
173
|
+
return sharp(buff);
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixeli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "A lightweight command-line tool for merging multiple images into customizable grid layouts.",
|
|
5
5
|
"homepage": "https://github.com/pakdad-mousavi/pixeli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -10,16 +10,10 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/pakdad-mousavi/pixeli.git"
|
|
12
12
|
},
|
|
13
|
-
"licenses": [
|
|
14
|
-
{
|
|
15
|
-
"type": "MIT",
|
|
16
|
-
"url": "https://github.com/pakdad-mousavi/pixeli/blob/main/LICENSE"
|
|
17
|
-
}
|
|
18
|
-
],
|
|
19
13
|
"bin": {
|
|
20
14
|
"pixeli": "bin/pixeli.js"
|
|
21
15
|
},
|
|
22
|
-
"license": "
|
|
16
|
+
"license": "MIT",
|
|
23
17
|
"author": "Pakdad Mousavi",
|
|
24
18
|
"type": "module",
|
|
25
19
|
"main": "index.js",
|
|
@@ -42,6 +36,7 @@
|
|
|
42
36
|
"chalk": "^5.6.2",
|
|
43
37
|
"cli-progress": "^3.12.0",
|
|
44
38
|
"commander": "^14.0.2",
|
|
45
|
-
"sharp": "^0.34.5"
|
|
39
|
+
"sharp": "^0.34.5",
|
|
40
|
+
"table": "^6.9.0"
|
|
46
41
|
}
|
|
47
42
|
}
|
|
File without changes
|