pulp-image 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/LICENSE +22 -0
- package/README.md +332 -0
- package/bin/pulp.js +251 -0
- package/package.json +37 -0
- package/src/banner.js +10 -0
- package/src/buildOutputPath.js +67 -0
- package/src/formats.js +54 -0
- package/src/planTasks.js +42 -0
- package/src/processImage.js +216 -0
- package/src/reporter.js +115 -0
- package/src/runJob.js +97 -0
- package/src/stats.js +30 -0
- package/src/uiServer.js +398 -0
- package/ui/app.js +1153 -0
- package/ui/assets/pulp-logo-white.svg +13 -0
- package/ui/index.html +475 -0
- package/ui/styles.css +929 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rebellion Geeks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# pulp-image
|
|
2
|
+
|
|
3
|
+
A powerful, safety-first CLI tool for processing images with resize, format conversion, and optimization.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- š¼ļø **Resize images** with aspect ratio preservation
|
|
12
|
+
- š **Convert formats** (PNG, JPG, WebP, AVIF)
|
|
13
|
+
- š¦ **Batch process** entire directories
|
|
14
|
+
- š”ļø **Safety-first** defaults (no overwrites, no accidental deletions)
|
|
15
|
+
- š **Detailed statistics** and summary reports
|
|
16
|
+
- šØ **Transparency handling** with customizable backgrounds
|
|
17
|
+
- ā” **Optimization** with quality and lossless options
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
### From GitHub
|
|
22
|
+
|
|
23
|
+
1. **Clone or download the repository:**
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/rebelliongeeks/pulp-image.git
|
|
26
|
+
cd pulp-image
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. **Install dependencies:**
|
|
30
|
+
```bash
|
|
31
|
+
npm install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. **Link the CLI globally:**
|
|
35
|
+
```bash
|
|
36
|
+
npm link
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This will make the `pulp` command available globally on your system.
|
|
40
|
+
|
|
41
|
+
### Alternative: Install directly from GitHub
|
|
42
|
+
|
|
43
|
+
You can also install directly from GitHub without cloning:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g github:rebelliongeeks/pulp-image
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Global Installation (Persistent)
|
|
50
|
+
|
|
51
|
+
If you want the `pulp` command to work even after deleting the project folder, use global installation instead of `npm link`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This copies the files to your global npm directory, so the command persists even if you delete the source folder.
|
|
58
|
+
|
|
59
|
+
**Note:** `npm link` creates a symlink (breaks if you delete the folder). `npm install -g` copies files (persists after deletion).
|
|
60
|
+
|
|
61
|
+
### Requirements
|
|
62
|
+
|
|
63
|
+
- Node.js >= 18.0.0
|
|
64
|
+
- npm (comes with Node.js)
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Resize an image
|
|
70
|
+
pulp image.png --width 800
|
|
71
|
+
|
|
72
|
+
# Convert format
|
|
73
|
+
pulp image.png --format webp
|
|
74
|
+
|
|
75
|
+
# Process all images in a directory
|
|
76
|
+
pulp ./images --format webp --out ./output
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pulp [input] [options]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Arguments
|
|
86
|
+
|
|
87
|
+
- `[input]` - Input file or directory (required)
|
|
88
|
+
|
|
89
|
+
### Common Options
|
|
90
|
+
|
|
91
|
+
#### Resize
|
|
92
|
+
- `-w, --width <number>` - Output width in pixels (preserves aspect ratio if height not specified)
|
|
93
|
+
- `-h, --height <number>` - Output height in pixels (preserves aspect ratio if width not specified)
|
|
94
|
+
|
|
95
|
+
#### Format
|
|
96
|
+
- `-f, --format <format>` - Output format: `png`, `jpg`, `webp`, or `avif`
|
|
97
|
+
|
|
98
|
+
#### Output
|
|
99
|
+
- `-o, --out <dir>` - Output directory (default: `./pulp-image-results`)
|
|
100
|
+
- **Why use --out?** This keeps your original files safe and organizes processed images in a dedicated folder. The output directory is created automatically if it doesn't exist.
|
|
101
|
+
|
|
102
|
+
#### Quality
|
|
103
|
+
- `--quality <number>` - Quality for lossy formats (1-100). Defaults: JPG=80, WebP=80, AVIF=50
|
|
104
|
+
- `--lossless` - Use lossless compression where supported (PNG, WebP, AVIF)
|
|
105
|
+
|
|
106
|
+
**Compression Behavior:**
|
|
107
|
+
- **PNG**: Always lossless (no quality setting available)
|
|
108
|
+
- **JPG**: Always lossy, default quality 80 (does not support lossless)
|
|
109
|
+
- **WebP**: Lossy by default (quality 80), can use `--lossless` for lossless compression
|
|
110
|
+
- **AVIF**: Lossy by default (quality 50), can use `--lossless` for lossless compression
|
|
111
|
+
|
|
112
|
+
**Note:** Resize operations do NOT affect compression. Resizing only changes image dimensions. Compression/quality is determined by the format and quality settings.
|
|
113
|
+
|
|
114
|
+
#### Transparency
|
|
115
|
+
- `--background <color>` - Background color for flattening transparency (hex format, default: `#ffffff`)
|
|
116
|
+
- `--alpha-mode <mode>` - Alpha handling: `flatten` (default) or `error`
|
|
117
|
+
|
|
118
|
+
#### Safety
|
|
119
|
+
- `--overwrite` - Overwrite existing output files (default: skip existing files)
|
|
120
|
+
- `--delete-original` - Delete original files after successful processing (only if paths differ)
|
|
121
|
+
|
|
122
|
+
#### Other
|
|
123
|
+
- `-v, --verbose` - Show detailed per-file processing information
|
|
124
|
+
- `--help` - Display help information
|
|
125
|
+
- `--version` - Show version number
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
### Resize Images
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Resize by width (preserves aspect ratio)
|
|
133
|
+
pulp image.png --width 800
|
|
134
|
+
|
|
135
|
+
# Resize by height (preserves aspect ratio)
|
|
136
|
+
pulp image.png --height 600
|
|
137
|
+
|
|
138
|
+
# Resize to exact dimensions
|
|
139
|
+
pulp image.png --width 800 --height 600
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Format Conversion
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Convert PNG to WebP (preserves transparency)
|
|
146
|
+
pulp image.png --format webp
|
|
147
|
+
|
|
148
|
+
# Convert PNG to JPG (flattens transparency to white)
|
|
149
|
+
pulp image.png --format jpg
|
|
150
|
+
|
|
151
|
+
# Convert with custom background color
|
|
152
|
+
pulp image.png --format jpg --background "#ff0000"
|
|
153
|
+
|
|
154
|
+
# Convert to lossless WebP
|
|
155
|
+
pulp image.png --format webp --lossless
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Suffix Options
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Auto suffix (adds size to filename)
|
|
162
|
+
pulp image.png --width 800 --auto-suffix
|
|
163
|
+
# Output: image-800w.png
|
|
164
|
+
|
|
165
|
+
# Custom suffix
|
|
166
|
+
pulp image.png --suffix "optimized"
|
|
167
|
+
# Output: image-optimized.png
|
|
168
|
+
|
|
169
|
+
# Combined suffixes (auto first, custom second)
|
|
170
|
+
pulp image.png --width 800 --auto-suffix --suffix "thumb"
|
|
171
|
+
# Output: image-800w-thumb.png
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Batch Processing
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Process all images in a directory
|
|
178
|
+
pulp ./images --format webp --out ./output
|
|
179
|
+
|
|
180
|
+
# Resize all images with auto-suffix
|
|
181
|
+
pulp ./images --width 800 --auto-suffix --out ./output
|
|
182
|
+
|
|
183
|
+
# Verbose mode (shows per-file details)
|
|
184
|
+
pulp ./images --format webp --out ./output --verbose
|
|
185
|
+
|
|
186
|
+
# Overwrite existing files
|
|
187
|
+
pulp ./images --format webp --out ./output --overwrite
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Safety Features
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Safe processing (output to separate directory)
|
|
194
|
+
pulp image.png --format webp --out ./pulp-image-results
|
|
195
|
+
|
|
196
|
+
# Overwrite existing output files
|
|
197
|
+
pulp image.png --format webp --out ./pulp-image-results --overwrite
|
|
198
|
+
|
|
199
|
+
# Delete original after successful processing
|
|
200
|
+
pulp image.png --format webp --out ./pulp-image-results --delete-original
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Safety Features
|
|
204
|
+
|
|
205
|
+
pulp-image is designed with safety in mind:
|
|
206
|
+
|
|
207
|
+
- **No overwrites by default** - Existing output files are skipped unless `--overwrite` is used
|
|
208
|
+
- **Delete original safety** - Only deletes after successful processing and only if input/output paths differ
|
|
209
|
+
- **Same path protection** - Prevents accidental in-place overwrites
|
|
210
|
+
- **Batch resilience** - Continues processing even if individual files fail
|
|
211
|
+
- **Clear error messages** - Helpful guidance when something goes wrong
|
|
212
|
+
|
|
213
|
+
## Output Directory (--out)
|
|
214
|
+
|
|
215
|
+
The `--out` option specifies where processed images are saved. Here's why it's useful:
|
|
216
|
+
|
|
217
|
+
- **Keeps originals safe** - Your source files remain untouched
|
|
218
|
+
- **Organizes outputs** - All processed images in one place
|
|
219
|
+
- **Prevents accidents** - No risk of overwriting originals
|
|
220
|
+
- **Default location** - Uses `./pulp-image-results` if not specified
|
|
221
|
+
- **Auto-creation** - Creates the directory if it doesn't exist
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
```bash
|
|
225
|
+
# Process images and save to ./output directory
|
|
226
|
+
pulp ./images --format webp --out ./output
|
|
227
|
+
|
|
228
|
+
# Your originals in ./images remain safe
|
|
229
|
+
# Processed images go to ./output
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Supported Formats
|
|
233
|
+
|
|
234
|
+
### Input Formats
|
|
235
|
+
- PNG
|
|
236
|
+
- JPG/JPEG
|
|
237
|
+
- WebP
|
|
238
|
+
- AVIF
|
|
239
|
+
|
|
240
|
+
### Output Formats
|
|
241
|
+
- PNG
|
|
242
|
+
- JPG
|
|
243
|
+
- WebP
|
|
244
|
+
- AVIF
|
|
245
|
+
|
|
246
|
+
## Compression & Quality
|
|
247
|
+
|
|
248
|
+
### Default Compression Behavior
|
|
249
|
+
|
|
250
|
+
Each format has different default compression behavior:
|
|
251
|
+
|
|
252
|
+
- **PNG**: Always lossless (no quality setting available)
|
|
253
|
+
- **JPG**: Always lossy, default quality 80 (does not support lossless)
|
|
254
|
+
- **WebP**: Lossy by default (quality 80), supports lossless with `--lossless`
|
|
255
|
+
- **AVIF**: Lossy by default (quality 50), supports lossless with `--lossless`
|
|
256
|
+
|
|
257
|
+
**Important:** Resize operations do NOT affect compression. Resizing only changes image dimensions. Compression/quality is determined by the format and quality settings.
|
|
258
|
+
|
|
259
|
+
### Using Lossless Compression
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
# Lossless WebP
|
|
263
|
+
pulp image.png --format webp --lossless
|
|
264
|
+
|
|
265
|
+
# Lossless AVIF
|
|
266
|
+
pulp image.png --format avif --lossless
|
|
267
|
+
|
|
268
|
+
# PNG is already lossless (no flag needed)
|
|
269
|
+
pulp image.jpg --format png
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Custom Quality Settings
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# High quality JPG
|
|
276
|
+
pulp image.png --format jpg --quality 95
|
|
277
|
+
|
|
278
|
+
# Lower quality WebP (smaller file)
|
|
279
|
+
pulp image.png --format webp --quality 60
|
|
280
|
+
|
|
281
|
+
# Custom AVIF quality
|
|
282
|
+
pulp image.png --format avif --quality 70
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Transparency Handling
|
|
286
|
+
|
|
287
|
+
When converting images with transparency to formats that don't support it (like JPG):
|
|
288
|
+
|
|
289
|
+
- **Default behavior**: Flattens transparency onto white background
|
|
290
|
+
- **Custom background**: Use `--background "#color"` to specify a color
|
|
291
|
+
- **Error mode**: Use `--alpha-mode error` to fail instead of flattening
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
```bash
|
|
295
|
+
# Flatten transparency to red background
|
|
296
|
+
pulp image.png --format jpg --background "#ff0000"
|
|
297
|
+
|
|
298
|
+
# Fail if transparency exists
|
|
299
|
+
pulp image.png --format jpg --alpha-mode error
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Statistics and Reporting
|
|
303
|
+
|
|
304
|
+
pulp-image provides detailed statistics:
|
|
305
|
+
|
|
306
|
+
- **Per-file stats**: Original size, final size, bytes saved, percent saved
|
|
307
|
+
- **Batch summary**: Total files processed, total sizes, overall savings
|
|
308
|
+
- **Skipped files**: Count and reasons (in verbose mode)
|
|
309
|
+
- **Failed files**: Count and errors (in verbose mode)
|
|
310
|
+
|
|
311
|
+
Example output:
|
|
312
|
+
```
|
|
313
|
+
ā Processed: 7 file(s)
|
|
314
|
+
Total original size: 10.07 MB
|
|
315
|
+
Total final size: 821.78 KB
|
|
316
|
+
Total saved: 9.26 MB (92.03%)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Interactive Documentation
|
|
320
|
+
|
|
321
|
+
For a beautiful interactive documentation with examples, open:
|
|
322
|
+
```
|
|
323
|
+
docs/index.html
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT
|
|
329
|
+
|
|
330
|
+
## Author
|
|
331
|
+
|
|
332
|
+
Rebellion Geeks
|
package/bin/pulp.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { dirname, resolve } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
const pkg = JSON.parse(
|
|
10
|
+
readFileSync(resolve(__dirname, '../package.json'), 'utf-8')
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import { Command } from 'commander';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { banner } from '../src/banner.js';
|
|
17
|
+
import { formatBytes } from '../src/stats.js';
|
|
18
|
+
import { planTasks } from '../src/planTasks.js';
|
|
19
|
+
import { Reporter } from '../src/reporter.js';
|
|
20
|
+
import { runJob } from '../src/runJob.js';
|
|
21
|
+
import { startUIServer } from '../src/uiServer.js';
|
|
22
|
+
import { statSync, existsSync } from 'fs';
|
|
23
|
+
|
|
24
|
+
const program = new Command();
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.name('pulp')
|
|
28
|
+
.description('A CLI tool for processing images with resize, format conversion, and optimization')
|
|
29
|
+
.version(pkg.version)
|
|
30
|
+
.addHelpText('before', chalk.cyan(banner))
|
|
31
|
+
.argument('[input]', 'Input file or directory')
|
|
32
|
+
.option('-w, --width <number>', 'Output width in pixels')
|
|
33
|
+
.option('-h, --height <number>', 'Output height in pixels')
|
|
34
|
+
.option('-f, --format <format>', 'Output format (png, jpg, webp, avif)')
|
|
35
|
+
.option('-o, --out <dir>', 'Output directory (default: ./pulp-image-results). Creates directory if it doesn\'t exist. Keeps originals safe by saving processed images separately.')
|
|
36
|
+
.option('--suffix <text>', 'Custom suffix to add before extension')
|
|
37
|
+
.option('--auto-suffix', 'Automatically add size-based suffix')
|
|
38
|
+
.option('--quality <number>', 'Quality for lossy formats (1-100). Defaults: JPG=80, WebP=80, AVIF=50')
|
|
39
|
+
.option('--lossless', 'Use lossless compression where supported (PNG, WebP, AVIF). Note: PNG is always lossless. JPG does not support lossless.')
|
|
40
|
+
.option('--background <color>', 'Background color for transparency flattening', '#ffffff')
|
|
41
|
+
.option('--alpha-mode <mode>', 'Alpha handling: flatten or error', 'flatten')
|
|
42
|
+
.option('--overwrite', 'Overwrite existing output files')
|
|
43
|
+
.option('--delete-original', 'Delete original files after processing')
|
|
44
|
+
.option('-v, --verbose', 'Verbose output')
|
|
45
|
+
.addHelpText('after', `
|
|
46
|
+
Examples:
|
|
47
|
+
$ pulp image.png --format webp --quality 95
|
|
48
|
+
$ pulp image.png --width 800
|
|
49
|
+
$ pulp image.png --format webp --out ./output
|
|
50
|
+
$ pulp image.png --format jpg --quality 95
|
|
51
|
+
$ pulp image.png --format webp --quality 60
|
|
52
|
+
$ pulp image.png --width 800 --height 600 --auto-suffix
|
|
53
|
+
$ pulp ./images --format webp --out ./output --verbose
|
|
54
|
+
$ pulp image.png --format jpg --background "#ff0000"
|
|
55
|
+
$ pulp image.png --format avif --quality 70
|
|
56
|
+
$ pulp ./images --width 800 --auto-suffix --suffix "thumb" --out ./pulp-image-results
|
|
57
|
+
|
|
58
|
+
About --out:
|
|
59
|
+
The --out option specifies where processed images are saved (default: ./pulp-image-results).
|
|
60
|
+
This keeps your original files safe and organizes outputs in a dedicated folder.
|
|
61
|
+
The output directory is created automatically if it doesn't exist.
|
|
62
|
+
|
|
63
|
+
Compression Behavior:
|
|
64
|
+
PNG: Always lossless (no quality setting)
|
|
65
|
+
JPG: Always lossy, default quality 80 (does not support lossless)
|
|
66
|
+
WebP: Lossy by default (quality 80), use --lossless for lossless
|
|
67
|
+
AVIF: Lossy by default (quality 50), use --lossless for lossless
|
|
68
|
+
Note: Resize does not affect compression - it only changes dimensions.
|
|
69
|
+
|
|
70
|
+
`)
|
|
71
|
+
.action(async (input, options) => {
|
|
72
|
+
// Display banner
|
|
73
|
+
console.log(chalk.cyan(banner));
|
|
74
|
+
|
|
75
|
+
// Normalize config
|
|
76
|
+
const config = {
|
|
77
|
+
input: input || null,
|
|
78
|
+
width: options.width ? parseInt(options.width, 10) : null,
|
|
79
|
+
height: options.height ? parseInt(options.height, 10) : null,
|
|
80
|
+
format: options.format || null,
|
|
81
|
+
out: options.out || './pulp-image-results',
|
|
82
|
+
suffix: options.suffix || null,
|
|
83
|
+
autoSuffix: options.autoSuffix || false,
|
|
84
|
+
quality: options.quality ? parseInt(options.quality, 10) : null,
|
|
85
|
+
lossless: options.lossless || false,
|
|
86
|
+
background: options.background || '#ffffff',
|
|
87
|
+
alphaMode: options.alphaMode || 'flatten',
|
|
88
|
+
overwrite: options.overwrite || false,
|
|
89
|
+
deleteOriginal: options.deleteOriginal || false,
|
|
90
|
+
verbose: options.verbose || false
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!input) {
|
|
94
|
+
console.log(chalk.yellow('\nNo input specified. Use --help for usage information.'));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Resolve input path
|
|
99
|
+
const inputPath = resolve(input);
|
|
100
|
+
|
|
101
|
+
if (!existsSync(inputPath)) {
|
|
102
|
+
console.error(chalk.red(`\nError: Input not found: ${inputPath}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const inputStats = statSync(inputPath);
|
|
107
|
+
const isDirectory = inputStats.isDirectory();
|
|
108
|
+
const isFile = inputStats.isFile();
|
|
109
|
+
|
|
110
|
+
if (!isFile && !isDirectory) {
|
|
111
|
+
console.error(chalk.red(`\nError: Input must be a file or directory: ${inputPath}`));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle directory planning for display (before runJob)
|
|
116
|
+
if (isDirectory) {
|
|
117
|
+
let tasks;
|
|
118
|
+
try {
|
|
119
|
+
tasks = planTasks(inputPath);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(chalk.red(`\nError reading directory: ${error.message}`));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (tasks.length === 0) {
|
|
126
|
+
console.log(chalk.yellow(`\nNo supported image files found in: ${inputPath}`));
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(chalk.gray(`\nFound ${tasks.length} image file(s) to process...\n`));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Use runJob for orchestration (no terminal output from runJob)
|
|
134
|
+
let results;
|
|
135
|
+
try {
|
|
136
|
+
if (isFile && config.verbose) {
|
|
137
|
+
console.log(chalk.gray(`\nProcessing: ${inputPath}`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
results = await runJob(inputPath, config);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
143
|
+
if (config.verbose) {
|
|
144
|
+
console.error(error);
|
|
145
|
+
}
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Display results exactly as before
|
|
150
|
+
if (isFile) {
|
|
151
|
+
// Single file: show detailed result or skip/error
|
|
152
|
+
if (results.processed.length > 0) {
|
|
153
|
+
const result = results.processed[0];
|
|
154
|
+
console.log(chalk.green(`\nā Processed: ${result.outputPath}`));
|
|
155
|
+
console.log(chalk.gray(` Original: ${formatBytes(result.originalSize)} (${result.metadata.width}x${result.metadata.height})`));
|
|
156
|
+
console.log(chalk.gray(` Final: ${formatBytes(result.finalSize)}`));
|
|
157
|
+
console.log(chalk.gray(` Saved: ${formatBytes(result.bytesSaved)} (${result.percentSaved}%)`));
|
|
158
|
+
|
|
159
|
+
if (result.deleteError) {
|
|
160
|
+
console.log(chalk.yellow(` Warning: Failed to delete original: ${result.deleteError}`));
|
|
161
|
+
}
|
|
162
|
+
} else if (results.skipped.length > 0) {
|
|
163
|
+
const skipped = results.skipped[0];
|
|
164
|
+
console.log(chalk.yellow(`\nā Skipped: ${skipped.filePath}`));
|
|
165
|
+
console.log(chalk.gray(` Reason: ${skipped.reason}`));
|
|
166
|
+
} else if (results.failed.length > 0) {
|
|
167
|
+
const failed = results.failed[0];
|
|
168
|
+
console.error(chalk.red(`\nā Error: ${failed.error}`));
|
|
169
|
+
if (config.verbose) {
|
|
170
|
+
console.error(failed);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// Directory: show per-file results in verbose mode, then summary
|
|
175
|
+
if (config.verbose) {
|
|
176
|
+
// Reconstruct reporter for per-file display
|
|
177
|
+
const reporter = new Reporter();
|
|
178
|
+
results.processed.forEach(r => reporter.recordProcessed(r));
|
|
179
|
+
results.skipped.forEach(s => reporter.recordSkipped(s.filePath, s.reason));
|
|
180
|
+
results.failed.forEach(f => reporter.recordFailed(f.filePath, new Error(f.error)));
|
|
181
|
+
|
|
182
|
+
// Show per-file results
|
|
183
|
+
results.processed.forEach(result => {
|
|
184
|
+
reporter.printFileResult(result, true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Show skipped files
|
|
188
|
+
results.skipped.forEach(({ filePath, reason }) => {
|
|
189
|
+
console.log(chalk.yellow(`ā Skipped: ${filePath}`));
|
|
190
|
+
console.log(chalk.gray(` Reason: ${reason}`));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Show failed files
|
|
194
|
+
results.failed.forEach(({ filePath, error }) => {
|
|
195
|
+
console.error(chalk.red(`ā Failed: ${filePath}`));
|
|
196
|
+
console.error(chalk.gray(` Error: ${error}`));
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
// Non-verbose: just show failures
|
|
200
|
+
results.failed.forEach(({ filePath, error }) => {
|
|
201
|
+
console.error(chalk.red(`ā Failed: ${filePath}`));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Print summary for batch processing or if there were issues
|
|
207
|
+
if (isDirectory || results.skipped.length > 0 || results.failed.length > 0) {
|
|
208
|
+
const reporter = new Reporter();
|
|
209
|
+
results.processed.forEach(r => reporter.recordProcessed(r));
|
|
210
|
+
results.skipped.forEach(s => reporter.recordSkipped(s.filePath, s.reason));
|
|
211
|
+
results.failed.forEach(f => reporter.recordFailed(f.filePath, new Error(f.error)));
|
|
212
|
+
reporter.printSummary(config.verbose);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// UI command
|
|
217
|
+
program
|
|
218
|
+
.command('ui')
|
|
219
|
+
.description('Start the browser-based UI')
|
|
220
|
+
.option('-p, --port <number>', 'Port to run server on', '3000')
|
|
221
|
+
.action(async (options) => {
|
|
222
|
+
const port = parseInt(options.port || '3000', 10);
|
|
223
|
+
|
|
224
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
225
|
+
console.error(chalk.red(`\nError: Invalid port number: ${options.port}`));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const { server } = await startUIServer(port);
|
|
231
|
+
|
|
232
|
+
// Handle graceful shutdown
|
|
233
|
+
const shutdown = () => {
|
|
234
|
+
console.log(chalk.gray('\n\nShutting down server...'));
|
|
235
|
+
server.close(() => {
|
|
236
|
+
console.log(chalk.green('Server stopped.'));
|
|
237
|
+
process.exit(0);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
process.on('SIGINT', shutdown);
|
|
242
|
+
process.on('SIGTERM', shutdown);
|
|
243
|
+
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error(chalk.red(`\nError starting UI server: ${error.message}`));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
program.parse();
|
|
251
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pulp-image",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A CLI tool for processing images with resize, format conversion, and optimization",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "bin/pulp.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pulp": "./bin/pulp.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"image",
|
|
15
|
+
"resize",
|
|
16
|
+
"convert",
|
|
17
|
+
"optimize",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"author": "Rebellion Geeks",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/rebelliongeeks/pulp-image.git"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"chalk": "^5.6.2",
|
|
31
|
+
"commander": "^14.0.2",
|
|
32
|
+
"express": "^4.18.2",
|
|
33
|
+
"multer": "^2.0.2",
|
|
34
|
+
"open": "^8.4.2",
|
|
35
|
+
"sharp": "^0.34.5"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/banner.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const banner = `
|
|
2
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
3
|
+
ā ā
|
|
4
|
+
ā pulp-image v0.1.0 ā
|
|
5
|
+
ā ā
|
|
6
|
+
ā Image processing made simple ā
|
|
7
|
+
ā ā
|
|
8
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
9
|
+
`;
|
|
10
|
+
|