pixeli 0.1.3 → 0.1.5
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
# Pixeli (Pre-release) [ [](https://www.npmjs.com/package/pixeli) [](./LICENSE)
|
|
3
3
|
|
|
4
4
|
<img src="./assets/logo.svg" width="150" align="right">
|
|
5
5
|
|
|
@@ -15,18 +15,23 @@ The tool currently supports two main layout modes: ***Grid*** and ***Masonry***
|
|
|
15
15
|
| **Masonry (Horizontal)** | **Masonry (Vertical)** |
|
|
16
16
|
| <img src="samples/masonry-horizontal.png" width="400"> | <img src="samples/masonry-vertical.png" width="400"> |
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
Pixeli can be installed using
|
|
18
|
+
## Installation
|
|
19
|
+
Pixeli can be installed using npm. Simply run the following command to install it globally on your machine:
|
|
20
20
|
```bash
|
|
21
21
|
npm i -g pixeli
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
You can also run pixeli directly with npx without installing it globally. This is convenient for quick experiments or one-off usage:
|
|
25
|
+
```bash
|
|
26
|
+
npx pixeli merge <subcommand> [options] <files...>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Examples
|
|
25
30
|
To run these examples, you can visit the [GitHub Repository](https://github.com/pakdad-mousavi/pixeli) and use the images in the [Samples](https://github.com/pakdad-mousavi/pixeli/blob/main/samples/) directory, if you don't already have your own set of images.
|
|
26
31
|
|
|
27
32
|
All merge commands are under the `pixeli merge` command and can be used like so: `pixeli merge [merge-mode] [options]`
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
### Basic Grid
|
|
30
35
|
To create a basic grid with 1:1 images, you can use the grid merge command. You'll also need to provide the individual filepaths to use, or use the `-rd` (--recursive and --directory) flags to get all the images from the specified directory:
|
|
31
36
|
```bash
|
|
32
37
|
pixeli merge grid -rd ./samples/images
|
|
@@ -34,19 +39,19 @@ pixeli merge grid -rd ./samples/images
|
|
|
34
39
|
|
|
35
40
|
Without the `-r` flag, only the images in the directory will be scanned, and any sub-directories will be ignored.
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
### Grid with Rectangular Images
|
|
38
43
|
To create a grid with images that all have the same aspect ratio, you can specify the aspect ratio to use for all images using the `--ar` flag:
|
|
39
44
|
```bash
|
|
40
45
|
pixeli merge grid -rd ./samples/images --ar 16:9
|
|
41
46
|
```
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
### Grid with 8 Columns
|
|
44
49
|
You can also customize the number of columns that you'd like the final image to have using the `-c` flag, followed by the number of columns:
|
|
45
50
|
```bash
|
|
46
51
|
pixeli merge grid -rd ./samples/images -c 8
|
|
47
52
|
```
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
### Contact Sheet
|
|
50
55
|
Contact sheet style grids can also be made using pixeli. To include each file name under its respective image, the `--ca` flag can be used:
|
|
51
56
|
```bash
|
|
52
57
|
pixeli merge grid -rd ./samples/images --ca
|
|
@@ -57,21 +62,42 @@ The caption color can also be specified using the `--cc` flag, followed by a hex
|
|
|
57
62
|
pixeli merge grid -rd ./samples/images --ca --cc "#ff0000"
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
### Masonry Layout
|
|
61
66
|
To create a masonry style image, you can use the masonry merge command. The `-rd` flag is used to specify which directory to use, and the canvas width can be specified using the `--cvw` flag:
|
|
62
67
|
```bash
|
|
63
68
|
pixeli merge masonry -rd ./samples/images --cvw 4000
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
By default, the masonry merge command uses a horizontal
|
|
71
|
+
By default, the masonry merge command uses a horizontal flow, but a vertical one can be specified using the `-f` flag, followed by the `--cvh` to specify the canvas height:
|
|
67
72
|
```bash
|
|
68
|
-
pixeli merge masonry -rd ./samples/images
|
|
73
|
+
pixeli merge masonry -rd ./samples/images -f vertical --cvh 4000
|
|
69
74
|
```
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
## Full Documentation
|
|
77
|
+
|
|
78
|
+
### pixeli merge
|
|
79
|
+
Usage: `pixeli merge <subcommand> [options] <input...> -o <output>`
|
|
80
|
+
|
|
81
|
+
The merge command is what allows you to create grids and mosaics with your images.
|
|
82
|
+
| Subcommand | Description | Options |
|
|
83
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
84
|
+
| `grid` | Merge images into a uniform **rows × columns grid**, optionally with captions and per-image aspect ratios. | See [grid options table](#pixeli-merge-grid) |
|
|
85
|
+
| `masonry` | Merge images into a **dynamic masonry layout**, preserving natural image proportions. Supports vertical or horizontal flow and alignment options. | See [masonry options table](#pixeli-merge-masonry) |
|
|
86
|
+
|
|
87
|
+
The following options and flags are shared for all of the subcommands under the `pixeli merge` command:
|
|
88
|
+
| Option | Default | Description |
|
|
89
|
+
| ------------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
90
|
+
| `[files...]` | — | Image file paths to merge. You can specify multiple files or if you prefer directories, use `--dir`. |
|
|
91
|
+
| `-d`, `--dir <path>` | — | Path to a **directory containing images** to merge. Can be used instead of listing files individually. |
|
|
92
|
+
| `-r`, `--recursive` | `false` | Include **images in all subdirectories** of the specified directory recursively. |
|
|
93
|
+
| `--sh`, `--shuffle` | `false` | **Randomize the order** of images before merging. Useful for creating visually varied grids or collages. |
|
|
94
|
+
| `-g`, `--gap <px>` | `50` | **Spacing (in pixels) between images** in the layout. Applies to both horizontal and vertical gaps. |
|
|
95
|
+
| `--bg`, `--canvas-color <hex>` | `#ffffff` | Sets the **background color of the canvas**. Accepts HEX values (e.g., `#000000` for black). |
|
|
96
|
+
| `-o`, `--output <file>` | `./pixeli.png` | Path for the **merged output image**. The format is inferred from the file extension (`.png`, `.jpg`, `.webp`, etc.). |
|
|
97
|
+
|
|
72
98
|
|
|
73
|
-
|
|
74
|
-
Usage: `pixeli merge grid [options]
|
|
99
|
+
### pixeli merge grid
|
|
100
|
+
Usage: `pixeli merge grid [options] [files...]`
|
|
75
101
|
|
|
76
102
|
The grid mode arranges images into a clean, uniform grid with fixed columns and automatic row calculation. The table below displays all of the options available to this command:
|
|
77
103
|
| Option/Flag | Default | Description |
|
|
@@ -83,20 +109,20 @@ The grid mode arranges images into a clean, uniform grid with fixed columns and
|
|
|
83
109
|
| `--cc`, `--caption-color <hex>` | `#000000` | HEX color value for caption text (e.g., `#ffffff`, `#ff9900`). Affects all captions uniformly. |
|
|
84
110
|
| `--mcs`, `--max-caption-size <pt>` | `100` | Sets the **maximum allowed caption font size**. Useful when images are extremely large and the caption is not big enough. The renderer may auto-reduce the font size if necessary. |
|
|
85
111
|
|
|
86
|
-
|
|
87
|
-
Usage: `pixeli merge masonry [options]
|
|
112
|
+
### pixeli merge masonry
|
|
113
|
+
Usage: `pixeli merge masonry [options] [files...]`
|
|
88
114
|
|
|
89
115
|
The masonry mode preserves each image’s natural shape, creating an organic brick-wall layout similar to Pinterest boards.
|
|
90
116
|
|
|
91
|
-
| Option/Flag | Default | Description
|
|
92
|
-
| ---------------------------------------------------- | ----------------------- |
|
|
93
|
-
| `--rh`, `--row-height <px>` | *smallest input height* | Sets the **target height for all images in a row** when using `horizontal`
|
|
94
|
-
| `--cw`, `--column-width <px>` | *smallest input width* | Sets the **target width for all images in a column** when using `vertical`
|
|
95
|
-
| `--cvw`, `--canvas-width <px>` | – | Sets the **fixed width** of the final output canvas. Required when using a `horizontal`
|
|
96
|
-
| `--cvh`, `--canvas-height <px>` | – | Sets the **fixed height** of the final output canvas. Required when using a `vertical`
|
|
97
|
-
|
|
|
98
|
-
| `--ha`, `--h-align <left\|center\|right\|justified>` | `justified` | Controls **horizontal alignment** of rows when in `horizontal`
|
|
99
|
-
| `--va`, `--v-align <top\|middle\|bottom\|justified>` | `justified` | Controls **vertical alignment** of columns when in `vertical`
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
| Option/Flag | Default | Description |
|
|
118
|
+
| ---------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
119
|
+
| `--rh`, `--row-height <px>` | *smallest input height* | Sets the **target height for all images in a row** when using `horizontal` flow. Images are scaled proportionally based on this height. |
|
|
120
|
+
| `--cw`, `--column-width <px>` | *smallest input width* | Sets the **target width for all images in a column** when using `vertical` flow. Images are scaled proportionally based on this width. |
|
|
121
|
+
| `--cvw`, `--canvas-width <px>` | – | Sets the **fixed width** of the final output canvas. Required when using a `horizontal` flow to know when to break a row. |
|
|
122
|
+
| `--cvh`, `--canvas-height <px>` | – | Sets the **fixed height** of the final output canvas. Required when using a `vertical` flow to know when to break a column. |
|
|
123
|
+
| `-f`, `--flow <horizontal\|vertical>` | `horizontal` | Determines the **flow direction** of the masonry layout. `horizontal` creates rows of varying widths; `vertical` creates columns of varying heights. |
|
|
124
|
+
| `--ha`, `--h-align <left\|center\|right\|justified>` | `justified` | Controls **horizontal alignment** of rows when in `horizontal` flow. `justified` overfills each row and crops the final image to fill up the canvas. |
|
|
125
|
+
| `--va`, `--v-align <top\|middle\|bottom\|justified>` | `justified` | Controls **vertical alignment** of columns when in `vertical` flow. `justified` overfills each column and crops the final image to fill up the canvas. |
|
|
126
|
+
|
|
127
|
+
## License
|
|
102
128
|
This project is licensed under the [MIT License](./LICENSE).
|
package/commands/masonry.js
CHANGED
|
@@ -22,9 +22,9 @@ masonryCommand
|
|
|
22
22
|
.option('--cw, --column-width <px>', 'The width of each column, defaults to the smallest image width', null)
|
|
23
23
|
.option('--cvw, --canvas-width <px>', 'The width of the canvas', null)
|
|
24
24
|
.option('--cvh, --canvas-height <px>', 'The height of the canvas', null)
|
|
25
|
-
.option('
|
|
26
|
-
.option('--ha, --h-align <left|center|right|justified>', 'Horizontal alignment of the grid (for horizontal
|
|
27
|
-
.option('--va, --v-align <top|middle|bottom|justified>', 'Vertical alignment of the grid (for vertical
|
|
25
|
+
.option('-f, --flow <horizontal|vertical>', 'The flow of the masonry layout', 'horizontal')
|
|
26
|
+
.option('--ha, --h-align <left|center|right|justified>', 'Horizontal alignment of the grid (for horizontal flows)', null)
|
|
27
|
+
.option('--va, --v-align <top|middle|bottom|justified>', 'Vertical alignment of the grid (for vertical flows)', null)
|
|
28
28
|
.action((files, opts) => {
|
|
29
29
|
main(files, opts);
|
|
30
30
|
});
|
package/lib/helpers/utils.js
CHANGED
|
@@ -40,7 +40,7 @@ export const addSharedOptions = (cmd) => {
|
|
|
40
40
|
.option('-r, --recursive', 'Recursively include subdirectories', false)
|
|
41
41
|
.option('--sh, --shuffle', 'Shuffle up images to randomize order in the grid', false)
|
|
42
42
|
.option('-g, --gap <px>', 'Gap between images', 50)
|
|
43
|
-
.option('--bg, --canvas-color <hex>', 'Background color for canvas', '#ffffff')
|
|
43
|
+
.option('--bg, --canvas-color <hex|transparent>', 'Background color for canvas', '#ffffff')
|
|
44
44
|
.option('-o, --output <file>', 'Output file path', './pixeli.png');
|
|
45
45
|
};
|
|
46
46
|
|
|
@@ -43,15 +43,15 @@ export const validateSharedOptions = (sharedOptions) => {
|
|
|
43
43
|
export const validateMasonryOptions = (sharedOptions, masonryOptions) => {
|
|
44
44
|
// Extract params
|
|
45
45
|
const { gap } = sharedOptions;
|
|
46
|
-
const { rowHeight, columnWidth, canvasWidth, canvasHeight,
|
|
46
|
+
const { rowHeight, columnWidth, canvasWidth, canvasHeight, flow, hAlign, vAlign } = masonryOptions;
|
|
47
47
|
|
|
48
48
|
// Define orientations and alignments for validation
|
|
49
|
-
const
|
|
49
|
+
const FLOWS = ['horizontal', 'vertical'];
|
|
50
50
|
const HORIZONTAL_ALIGNMENTS = ['left', 'center', 'right', 'justified'];
|
|
51
51
|
const VERTICAL_ALIGNMENTS = ['top', 'middle', 'bottom', 'justified'];
|
|
52
52
|
|
|
53
53
|
// Define orientation dependent options which are ignored if defined for the wrong orientation
|
|
54
|
-
const
|
|
54
|
+
const IGNORED_FLOW_DEPENDENT_OPTIONS = {
|
|
55
55
|
horizontal: [
|
|
56
56
|
{
|
|
57
57
|
option: '--v-align',
|
|
@@ -83,8 +83,8 @@ export const validateMasonryOptions = (sharedOptions, masonryOptions) => {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
// Validate orientations and alignments
|
|
86
|
-
if (!
|
|
87
|
-
throw new Error('Invalid orientation. Choose one of the following: ' +
|
|
86
|
+
if (!FLOWS.includes(flow)) {
|
|
87
|
+
throw new Error('Invalid orientation. Choose one of the following: ' + FLOWS.join(', '));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
if (hAlign && !HORIZONTAL_ALIGNMENTS.includes(hAlign)) {
|
|
@@ -105,43 +105,37 @@ export const validateMasonryOptions = (sharedOptions, masonryOptions) => {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// Ensure canvas width is given
|
|
108
|
-
if (
|
|
108
|
+
if (flow === 'horizontal' && !canvasWidth) {
|
|
109
109
|
throw new Error('--canvas-width must be given.');
|
|
110
110
|
}
|
|
111
111
|
// and is a positive integer
|
|
112
|
-
else if (
|
|
113
|
-
orientation === 'horizontal' &&
|
|
114
|
-
(isNaN(canvasWidth) || !Number.isInteger(Number(canvasWidth)) || Number(canvasWidth) < 1)
|
|
115
|
-
) {
|
|
112
|
+
else if (flow === 'horizontal' && (isNaN(canvasWidth) || !Number.isInteger(Number(canvasWidth)) || Number(canvasWidth) < 1)) {
|
|
116
113
|
throw new Error('--canvas-width must be a positive integer.');
|
|
117
114
|
}
|
|
118
115
|
// and it accomodates for the minimum width needed
|
|
119
|
-
else if (
|
|
116
|
+
else if (flow === 'horizontal' && canvasWidth <= gap * 2) {
|
|
120
117
|
throw new Error(`--canvas-width must be greater than 2 gaps or ${gap * 2}px.`);
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
// Ensure canvas height is given
|
|
124
|
-
if (
|
|
121
|
+
if (flow === 'vertical' && !canvasHeight) {
|
|
125
122
|
throw new Error('--canvas-height must be given.');
|
|
126
123
|
}
|
|
127
124
|
// and is a positive integer
|
|
128
|
-
else if (
|
|
129
|
-
orientation === 'vertical' &&
|
|
130
|
-
(isNaN(canvasHeight) || !Number.isInteger(Number(canvasHeight)) || Number(canvasHeight) < 1)
|
|
131
|
-
) {
|
|
125
|
+
else if (flow === 'vertical' && (isNaN(canvasHeight) || !Number.isInteger(Number(canvasHeight)) || Number(canvasHeight) < 1)) {
|
|
132
126
|
throw new Error('--canvas-height must be a positive integer.');
|
|
133
127
|
}
|
|
134
128
|
// and it accomodates for the minimum height needed
|
|
135
|
-
else if (
|
|
129
|
+
else if (flow === 'vertical' && canvasHeight <= gap * 2) {
|
|
136
130
|
throw new Error(`--canvas-height must be greater than 2 gaps or ${gap * 2}px.`);
|
|
137
131
|
}
|
|
138
132
|
|
|
139
133
|
// Validate dependent options by showing warnings when incorrect parameters
|
|
140
134
|
// are used with incorrect orientation
|
|
141
|
-
const
|
|
142
|
-
for (const { option, value } of
|
|
135
|
+
const ignoredFlowOptions = IGNORED_FLOW_DEPENDENT_OPTIONS[flow];
|
|
136
|
+
for (const { option, value } of ignoredFlowOptions) {
|
|
143
137
|
if (value) {
|
|
144
|
-
displayWarningMessage(`"${option}" option is ignored due to ${
|
|
138
|
+
displayWarningMessage(`"${option}" option is ignored due to ${flow} flow.`);
|
|
145
139
|
}
|
|
146
140
|
}
|
|
147
141
|
|
|
@@ -150,7 +144,7 @@ export const validateMasonryOptions = (sharedOptions, masonryOptions) => {
|
|
|
150
144
|
columnWidth: Number(columnWidth) || null,
|
|
151
145
|
canvasHeight: Number(canvasHeight) || null,
|
|
152
146
|
canvasWidth: Number(canvasWidth) || null,
|
|
153
|
-
|
|
147
|
+
flow,
|
|
154
148
|
hAlign,
|
|
155
149
|
vAlign,
|
|
156
150
|
};
|
|
@@ -2,7 +2,7 @@ import { calculateAvgWidth, calculateAvgHeight } from '../merge-utils.js';
|
|
|
2
2
|
import { buildHorizontalMasonry } from './horizontal.js';
|
|
3
3
|
import { buildVerticalMasonry } from './vertical.js';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const FLOW_DEFAULTS = {
|
|
6
6
|
horizontal: {
|
|
7
7
|
needed: ['canvasWidth', 'rowHeight', 'hAlign'],
|
|
8
8
|
defaults: {
|
|
@@ -20,15 +20,15 @@ const ORIENTATION_DEFAULTS = {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
export const masonryMerge = async (images, opts) => {
|
|
23
|
-
const {
|
|
24
|
-
const params = await
|
|
23
|
+
const { flow } = opts;
|
|
24
|
+
const params = await getFlowSpecificParams(images, opts);
|
|
25
25
|
|
|
26
|
-
return await generateGrid(
|
|
26
|
+
return await generateGrid(flow, images, params);
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
const {
|
|
31
|
-
const config =
|
|
29
|
+
const getFlowSpecificParams = async (images, currentParams) => {
|
|
30
|
+
const { flow, gap, canvasColor } = currentParams;
|
|
31
|
+
const config = FLOW_DEFAULTS[flow];
|
|
32
32
|
|
|
33
33
|
const output = { gap, canvasColor };
|
|
34
34
|
|
|
@@ -48,8 +48,8 @@ const getOrientationSpecificParams = async (images, currentParams) => {
|
|
|
48
48
|
return output;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
const generateGrid = async (
|
|
52
|
-
if (
|
|
51
|
+
const generateGrid = async (flow, images, params) => {
|
|
52
|
+
if (flow === 'horizontal') {
|
|
53
53
|
return await buildHorizontalMasonry(images, params);
|
|
54
54
|
} else {
|
|
55
55
|
return buildVerticalMasonry(images, params);
|
|
@@ -16,10 +16,6 @@ export const buildVerticalMasonry = async (images, params) => {
|
|
|
16
16
|
|
|
17
17
|
// Split images into columns, then calculate canvasWidth
|
|
18
18
|
const columns = await splitIntoColumns(scaledImages, canvasHeight, gap, vAlign);
|
|
19
|
-
console.log(columns.length);
|
|
20
|
-
columns.forEach((col) => {
|
|
21
|
-
console.log(col.length);
|
|
22
|
-
});
|
|
23
19
|
const canvasWidth = columns.length * columnWidth + (columns.length + 1) * gap;
|
|
24
20
|
|
|
25
21
|
// Create and return grid of images
|
|
@@ -63,9 +59,6 @@ const createMasonryLayout = async (cols, columnWidth, canvasWidth, canvasHeight,
|
|
|
63
59
|
const buffer = await finalizedImage.resize(resizeOptions).toBuffer();
|
|
64
60
|
finalizedImage = sharp(buffer);
|
|
65
61
|
finalizedMeta = await finalizedImage.metadata();
|
|
66
|
-
|
|
67
|
-
// Update progress bar
|
|
68
|
-
progressBar.increment();
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
composites.push({
|
|
@@ -75,6 +68,9 @@ const createMasonryLayout = async (cols, columnWidth, canvasWidth, canvasHeight,
|
|
|
75
68
|
});
|
|
76
69
|
|
|
77
70
|
y += finalizedMeta.height + gap;
|
|
71
|
+
|
|
72
|
+
// Update progress bar
|
|
73
|
+
progressBar.increment();
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
y = gap;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixeli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"bin": {
|
|
14
14
|
"pixeli": "bin/pixeli.js"
|
|
15
15
|
},
|
|
16
|
-
"license": "
|
|
16
|
+
"license": "MIT",
|
|
17
17
|
"author": "Pakdad Mousavi",
|
|
18
18
|
"type": "module",
|
|
19
19
|
"main": "index.js",
|