picslim 1.0.0 → 2.0.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 +26 -34
- package/package.json +8 -2
- package/picslim.js +95 -135
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# PicSlim
|
|
2
2
|
|
|
3
|
-
**Picslim** is a Node.js package that
|
|
3
|
+
**Picslim** is a robust Node.js package that efficiently optimizes images within a specified directory (recursively). It supports **JPEG**, **PNG**, **WebP**, and **AVIF** formats, offering fine-grained control over image quality, resizing, and format conversion.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- **Fast**: Uses parallel processing to utilize all CPU cores.
|
|
7
|
+
- **Recursive**: Automatically finds images in subdirectories.
|
|
8
|
+
- **Modern Formats**: Optional support for generating **WebP** and **AVIF**.
|
|
9
|
+
- **Smart Resizing**: Maintains aspect ratio and avoids enlarging images.
|
|
4
10
|
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
@@ -10,7 +16,7 @@ You can install picslim globally using npm:
|
|
|
10
16
|
npm install -g picslim
|
|
11
17
|
```
|
|
12
18
|
|
|
13
|
-
or
|
|
19
|
+
or run it directly with npx:
|
|
14
20
|
|
|
15
21
|
```bash
|
|
16
22
|
npx picslim
|
|
@@ -18,9 +24,9 @@ npx picslim
|
|
|
18
24
|
|
|
19
25
|
# Usage
|
|
20
26
|
|
|
21
|
-
Once installed, you can use the
|
|
27
|
+
Once installed, you can use the `picslim` command in your terminal:
|
|
22
28
|
|
|
23
|
-
```
|
|
29
|
+
```bash
|
|
24
30
|
picslim [options]
|
|
25
31
|
```
|
|
26
32
|
|
|
@@ -33,10 +39,11 @@ picslim [options]
|
|
|
33
39
|
- `-h, --maxHeight <number>`: Maximum height allowed for images.
|
|
34
40
|
- `-i, --input <path>`: Path to the input directory.
|
|
35
41
|
- `-o, --output <path>`: Path to the output directory.
|
|
42
|
+
- `-f, --formats <list>`: Comma-separated list of output formats (default: Source only). Options: `source`, `webp`, `avif`.
|
|
36
43
|
|
|
37
44
|
### Configuration File
|
|
38
45
|
|
|
39
|
-
You can create a `config.json` file in your project directory to specify default settings.
|
|
46
|
+
You can create a `config.json` file in your project directory to specify default settings.
|
|
40
47
|
|
|
41
48
|
```json
|
|
42
49
|
{
|
|
@@ -45,49 +52,34 @@ You can create a `config.json` file in your project directory to specify default
|
|
|
45
52
|
"quality": 80,
|
|
46
53
|
"maxWidth": null,
|
|
47
54
|
"maxHeight": null,
|
|
48
|
-
"compressionLevel": 9
|
|
55
|
+
"compressionLevel": 9,
|
|
56
|
+
"formats": ["source"]
|
|
49
57
|
}
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
###
|
|
53
|
-
|
|
54
|
-
Optimize images using default settings from the configuration file:
|
|
60
|
+
### Examples
|
|
55
61
|
|
|
62
|
+
**1. Basic Optimization (Default)**
|
|
63
|
+
Optimizes images in the current directory and saves them to `./min`. Keeps original format.
|
|
56
64
|
```bash
|
|
57
65
|
picslim
|
|
58
66
|
```
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
**2. Custom Input/Output and Resizing**
|
|
62
69
|
```bash
|
|
63
|
-
picslim -
|
|
70
|
+
picslim -i ./assets -o ./build/assets -w 800
|
|
64
71
|
```
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
### Example
|
|
69
|
-
|
|
70
|
-
Suppose you have a directory with the following images:
|
|
71
|
-
|
|
72
|
-
- image1.jpg
|
|
73
|
-
- image2.jpg
|
|
74
|
-
- image3.png
|
|
75
|
-
- image4.png
|
|
76
|
-
|
|
77
|
-
You can optimize all these images with the following command:
|
|
78
|
-
|
|
73
|
+
**3. Generate WebP Only**
|
|
74
|
+
Converts all images to WebP format.
|
|
79
75
|
```bash
|
|
80
|
-
picslim -
|
|
76
|
+
picslim -f webp
|
|
81
77
|
```
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
-
|
|
87
|
-
- image1.jpg
|
|
88
|
-
- image2.jpg
|
|
89
|
-
- image3.png
|
|
90
|
-
- image4.png
|
|
79
|
+
**4. Generate Optimized Source + AVIF**
|
|
80
|
+
Keeps the original format (optimized) AND generates an AVIF version.
|
|
81
|
+
```bash
|
|
82
|
+
picslim -f source,avif
|
|
91
83
|
```
|
|
92
84
|
|
|
93
85
|
### License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picslim",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Picslim is a Node.js package that allows you to optimize images in a specified directory.",
|
|
5
5
|
"main": "picslim.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
},
|
|
9
9
|
"source": "picslim.js",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"start": "node picslim.js
|
|
11
|
+
"start": "node picslim.js -q 80",
|
|
12
|
+
"test": "node picslim.js -i ./in -o ./out",
|
|
13
|
+
"test:webp": "node picslim.js -i ./in -o ./out -f webp",
|
|
14
|
+
"test:all": "node picslim.js -i ./in -o ./out -f source,webp,avif",
|
|
15
|
+
"clean": "rm -rf ./out/*",
|
|
12
16
|
"format": "standard --fix",
|
|
13
17
|
"prepare": "husky install"
|
|
14
18
|
},
|
|
@@ -46,6 +50,8 @@
|
|
|
46
50
|
]
|
|
47
51
|
},
|
|
48
52
|
"dependencies": {
|
|
53
|
+
"chalk": "^4.1.2",
|
|
54
|
+
"cli-progress": "^3.12.0",
|
|
49
55
|
"sharp": "^0.32.6",
|
|
50
56
|
"yargs": "^17.7.2"
|
|
51
57
|
},
|
package/picslim.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const fs = require('fs').promises
|
|
3
|
-
const sharp = require('sharp')
|
|
4
2
|
const yargs = require('yargs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const chalk = require('chalk')
|
|
5
|
+
const cliProgress = require('cli-progress')
|
|
6
|
+
const { loadConfig } = require('./src/config')
|
|
7
|
+
const { walkDirectory, isImage } = require('./src/utils')
|
|
8
|
+
const { processImage } = require('./src/image-processor')
|
|
9
|
+
const os = require('os')
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Parses command line arguments using yargs library.
|
|
@@ -12,184 +17,139 @@ function parseArguments () {
|
|
|
12
17
|
alias: 'config',
|
|
13
18
|
describe: 'Path to the configuration file',
|
|
14
19
|
demandOption: false,
|
|
15
|
-
type: 'string'
|
|
16
|
-
default: 'config.json'
|
|
20
|
+
type: 'string'
|
|
17
21
|
},
|
|
18
22
|
q: {
|
|
19
23
|
alias: 'quality',
|
|
20
24
|
describe: 'Image quality',
|
|
21
|
-
|
|
22
|
-
type: 'number',
|
|
23
|
-
default: null
|
|
25
|
+
type: 'number'
|
|
24
26
|
},
|
|
25
27
|
l: {
|
|
26
28
|
alias: 'compressionLevel',
|
|
27
29
|
describe: 'PNG compression level',
|
|
28
|
-
|
|
29
|
-
type: 'number',
|
|
30
|
-
default: null
|
|
30
|
+
type: 'number'
|
|
31
31
|
},
|
|
32
32
|
w: {
|
|
33
33
|
alias: 'maxWidth',
|
|
34
34
|
describe: 'Maximum width allowed',
|
|
35
|
-
|
|
36
|
-
type: 'number',
|
|
37
|
-
default: null
|
|
35
|
+
type: 'number'
|
|
38
36
|
},
|
|
39
37
|
h: {
|
|
40
38
|
alias: 'maxHeight',
|
|
41
39
|
describe: 'Maximum height allowed',
|
|
42
|
-
|
|
43
|
-
type: 'number',
|
|
44
|
-
default: null
|
|
40
|
+
type: 'number'
|
|
45
41
|
},
|
|
46
42
|
i: {
|
|
47
43
|
alias: 'input',
|
|
48
44
|
describe: 'Path to the input directory',
|
|
49
|
-
|
|
50
|
-
type: 'string',
|
|
51
|
-
default: null
|
|
45
|
+
type: 'string'
|
|
52
46
|
},
|
|
53
47
|
o: {
|
|
54
48
|
alias: 'output',
|
|
55
49
|
describe: 'Path to the output directory',
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
type: 'string'
|
|
51
|
+
},
|
|
52
|
+
f: {
|
|
53
|
+
alias: 'formats',
|
|
54
|
+
describe: 'Output formats (comma separated). Default: source. Options: source, webp, avif',
|
|
55
|
+
type: 'string'
|
|
59
56
|
}
|
|
60
57
|
}).argv
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
* @param {string} dir - The directory path to create.
|
|
66
|
-
* @returns {Promise<void>}
|
|
67
|
-
*/
|
|
68
|
-
async function createOutputDirectory (dir) {
|
|
69
|
-
try {
|
|
70
|
-
await fs.access(dir)
|
|
71
|
-
} catch (error) {
|
|
72
|
-
await fs.mkdir(dir)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
60
|
+
async function main () {
|
|
61
|
+
console.log(chalk.blue('picslim - Image Optimization Tool'))
|
|
75
62
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* @param {number} compressionLevel - PNG compression level.
|
|
86
|
-
*/
|
|
87
|
-
async function processImage (inputPath, outputPath, file, maxWidth, maxHeight, quality, compressionLevel) {
|
|
88
|
-
try {
|
|
89
|
-
const image = sharp(inputPath)
|
|
90
|
-
const metadata = await image.metadata()
|
|
91
|
-
const originalWidth = metadata.width
|
|
92
|
-
const originalHeight = metadata.height
|
|
93
|
-
const imageProcessor = image.resize(
|
|
94
|
-
originalWidth > maxWidth ? maxWidth : null,
|
|
95
|
-
originalHeight > maxHeight ? maxHeight : null
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
if (file.match(/\.(jpg|jpeg)$/i)) {
|
|
99
|
-
await imageProcessor.jpeg({ quality }).toFile(outputPath)
|
|
100
|
-
console.log(`Optimized JPEG image: ${file}`)
|
|
101
|
-
} else if (file.match(/\.(png)$/i)) {
|
|
102
|
-
await imageProcessor.png({ quality, compressionLevel }).toFile(outputPath)
|
|
103
|
-
console.log(`Optimized PNG image: ${file}`)
|
|
104
|
-
}
|
|
105
|
-
} catch (error) {
|
|
106
|
-
// console.error(`Optimization error ${file}: `, error)
|
|
63
|
+
const argv = parseArguments()
|
|
64
|
+
|
|
65
|
+
// Load config (merges defaults, config file, and argv)
|
|
66
|
+
const baseConfig = await loadConfig(argv.config)
|
|
67
|
+
|
|
68
|
+
// Parse formats from CLI if present
|
|
69
|
+
let formats = baseConfig.formats
|
|
70
|
+
if (argv.formats) {
|
|
71
|
+
formats = argv.formats.split(',').map(f => f.trim().toLowerCase())
|
|
107
72
|
}
|
|
108
|
-
}
|
|
109
73
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
config = JSON.parse(configData)
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error('Using the default configuration from the package. ✅')
|
|
123
|
-
config = require('./config.json') // Load default configuration from the package
|
|
74
|
+
// Overrides from argv
|
|
75
|
+
const config = {
|
|
76
|
+
...baseConfig,
|
|
77
|
+
inputDir: argv.input || baseConfig.inputDir,
|
|
78
|
+
outputDir: argv.output || baseConfig.outputDir,
|
|
79
|
+
quality: argv.quality || baseConfig.quality,
|
|
80
|
+
compressionLevel: argv.compressionLevel || baseConfig.compressionLevel,
|
|
81
|
+
maxWidth: argv.maxWidth || baseConfig.maxWidth,
|
|
82
|
+
maxHeight: argv.maxHeight || baseConfig.maxHeight,
|
|
83
|
+
formats
|
|
124
84
|
}
|
|
125
85
|
|
|
126
|
-
|
|
127
|
-
|
|
86
|
+
const inputDir = path.resolve(config.inputDir)
|
|
87
|
+
const outputDir = path.resolve(config.outputDir)
|
|
88
|
+
|
|
89
|
+
console.log(chalk.gray(`Input: ${inputDir}`))
|
|
90
|
+
console.log(chalk.gray(`Output: ${outputDir}`))
|
|
91
|
+
|
|
92
|
+
// Find files
|
|
93
|
+
console.log('Scanning files...')
|
|
94
|
+
let files = []
|
|
95
|
+
try {
|
|
96
|
+
files = await walkDirectory(inputDir)
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(chalk.red(`Error reading input directory: ${e.message}`))
|
|
128
99
|
process.exit(1)
|
|
129
100
|
}
|
|
130
101
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
102
|
+
// Filter images
|
|
103
|
+
const images = files.filter(isImage)
|
|
104
|
+
|
|
105
|
+
if (images.length === 0) {
|
|
106
|
+
console.log(chalk.yellow('No images found to process.'))
|
|
107
|
+
return
|
|
134
108
|
}
|
|
135
109
|
|
|
136
|
-
|
|
137
|
-
}
|
|
110
|
+
console.log(chalk.green(`Found ${images.length} images. Starting optimization...`))
|
|
138
111
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
* @param {object} config - The loaded configuration object.
|
|
143
|
-
*/
|
|
144
|
-
function validateConfigObject (config) {
|
|
145
|
-
return (
|
|
146
|
-
typeof config === 'object' &&
|
|
147
|
-
!Array.isArray(config) &&
|
|
148
|
-
config.inputDir &&
|
|
149
|
-
config.outputDir
|
|
150
|
-
)
|
|
151
|
-
}
|
|
112
|
+
// Prepare batch processing
|
|
113
|
+
const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic)
|
|
114
|
+
progressBar.start(images.length, 0)
|
|
152
115
|
|
|
153
|
-
|
|
154
|
-
* Returns the input directory path based on the provided arguments and configuration.
|
|
155
|
-
* If the provided arguments or configuration are '.', returns the current working directory.
|
|
156
|
-
* @param {string} argvInput - The input directory path provided as an argument.
|
|
157
|
-
* @param {string} configInputDir - The input directory path provided in the configuration.
|
|
158
|
-
* @returns {string} - The input directory path.
|
|
159
|
-
*/
|
|
160
|
-
function getInputDirectory (argvInput, configInputDir) {
|
|
161
|
-
if (argvInput === '.' || configInputDir === '.') {
|
|
162
|
-
return process.cwd()
|
|
163
|
-
}
|
|
116
|
+
const concurrencyLevel = os.cpus().length // Use number of CPU cores
|
|
164
117
|
|
|
165
|
-
|
|
166
|
-
|
|
118
|
+
let completed = 0
|
|
119
|
+
const errors = []
|
|
167
120
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const argv = parseArguments()
|
|
173
|
-
const config = await loadConfig(argv.config)
|
|
174
|
-
const inputDir = getInputDirectory(argv.input, config.inputDir)
|
|
175
|
-
const outputDir = getInputDirectory(argv.output, config.outputDir)
|
|
176
|
-
const quality = argv.quality ? argv.quality : config.quality
|
|
177
|
-
const maxWidth = argv.maxWidth ? argv.maxWidth : config.maxWidth
|
|
178
|
-
const maxHeight = argv.maxHeight ? argv.maxHeight : config.maxHeight
|
|
179
|
-
const compressionLevel = argv.compressionLevel ? argv.compressionLevel : config.compressionLevel
|
|
121
|
+
// Chunking helper
|
|
122
|
+
const chunk = (arr, size) => Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
|
|
123
|
+
arr.slice(i * size, i * size + size)
|
|
124
|
+
)
|
|
180
125
|
|
|
181
|
-
|
|
126
|
+
const batches = chunk(images, concurrencyLevel)
|
|
182
127
|
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
128
|
+
for (const batch of batches) {
|
|
129
|
+
const promises = batch.map(async (filePath) => {
|
|
130
|
+
const relativePath = path.relative(inputDir, filePath)
|
|
131
|
+
const result = await processImage(filePath, outputDir, relativePath, config)
|
|
132
|
+
completed++
|
|
133
|
+
progressBar.update(completed)
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
errors.push(result)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
await Promise.all(promises)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
progressBar.stop()
|
|
142
|
+
|
|
143
|
+
console.log('\n' + chalk.blue('--------------------------------------------------'))
|
|
144
|
+
console.log(chalk.green('Optimization complete!'))
|
|
145
|
+
console.log(`Processed: ${completed}`)
|
|
146
|
+
if (errors.length > 0) {
|
|
147
|
+
console.log(chalk.red(`Errors: ${errors.length}`))
|
|
148
|
+
errors.forEach(e => console.log(chalk.red(` - ${e.file}: ${e.error}`)))
|
|
149
|
+
} else {
|
|
150
|
+
console.log(chalk.green('No errors encountered.'))
|
|
192
151
|
}
|
|
152
|
+
console.log(chalk.blue('--------------------------------------------------'))
|
|
193
153
|
}
|
|
194
154
|
|
|
195
155
|
main()
|