picslim 0.0.8 → 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 +60 -32
- package/package.json +8 -2
- package/picslim.js +117 -126
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,52 +16,70 @@ You can install picslim globally using npm:
|
|
|
10
16
|
npm install -g picslim
|
|
11
17
|
```
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Once installed, you can use the optimizimage command in your terminal. Here's how you can use it:
|
|
19
|
+
or run it directly with npx:
|
|
16
20
|
|
|
21
|
+
```bash
|
|
22
|
+
npx picslim
|
|
17
23
|
```
|
|
18
|
-
picslim [options]
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Options:
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
**-mw, --maxWidth [value]:** Set the maximum width allowed (default: null).<br>
|
|
25
|
-
**-mh, --maxHeight [value]:** Set the maximum height allowed (default: null).<br>
|
|
26
|
-
**-c, --compressionLevel [value]:** Set the compression level (0 to 9, default: 9).
|
|
25
|
+
# Usage
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
Once installed, you can use the `picslim` command in your terminal:
|
|
29
28
|
|
|
30
29
|
```bash
|
|
31
|
-
picslim
|
|
30
|
+
picslim [options]
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
### Options
|
|
34
|
+
|
|
35
|
+
- `-c, --config <path>`: Path to the configuration file. (default: 'config.json')
|
|
36
|
+
- `-q, --quality <number>`: Image quality (0-100).
|
|
37
|
+
- `-l, --compressionLevel <number>`: PNG compression level (0-9).
|
|
38
|
+
- `-w, --maxWidth <number>`: Maximum width allowed for images.
|
|
39
|
+
- `-h, --maxHeight <number>`: Maximum height allowed for images.
|
|
40
|
+
- `-i, --input <path>`: Path to the input directory.
|
|
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`.
|
|
43
|
+
|
|
44
|
+
### Configuration File
|
|
45
|
+
|
|
46
|
+
You can create a `config.json` file in your project directory to specify default settings.
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"inputDir": "./in",
|
|
51
|
+
"outputDir": "./min",
|
|
52
|
+
"quality": 80,
|
|
53
|
+
"maxWidth": null,
|
|
54
|
+
"maxHeight": null,
|
|
55
|
+
"compressionLevel": 9,
|
|
56
|
+
"formats": ["source"]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
39
59
|
|
|
40
|
-
|
|
41
|
-
- image2.jpg
|
|
42
|
-
- image3.png
|
|
43
|
-
- image4.png
|
|
60
|
+
### Examples
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
**1. Basic Optimization (Default)**
|
|
63
|
+
Optimizes images in the current directory and saves them to `./min`. Keeps original format.
|
|
64
|
+
```bash
|
|
65
|
+
picslim
|
|
66
|
+
```
|
|
46
67
|
|
|
68
|
+
**2. Custom Input/Output and Resizing**
|
|
47
69
|
```bash
|
|
48
|
-
picslim -
|
|
70
|
+
picslim -i ./assets -o ./build/assets -w 800
|
|
49
71
|
```
|
|
50
72
|
|
|
51
|
-
|
|
73
|
+
**3. Generate WebP Only**
|
|
74
|
+
Converts all images to WebP format.
|
|
75
|
+
```bash
|
|
76
|
+
picslim -f webp
|
|
77
|
+
```
|
|
52
78
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- image3.png
|
|
58
|
-
- 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
|
|
59
83
|
```
|
|
60
84
|
|
|
61
85
|
### License
|
|
@@ -65,3 +89,7 @@ This project is licensed under the MIT License. See the LICENSE file for details
|
|
|
65
89
|
### Author
|
|
66
90
|
|
|
67
91
|
Ivan Mercedes
|
|
92
|
+
|
|
93
|
+
### Contributors
|
|
94
|
+
|
|
95
|
+
- [Elminson De Oleo Baez](https://github.com/elminson)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picslim",
|
|
3
|
-
"version": "0.0
|
|
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,153 +17,139 @@ function parseArguments () {
|
|
|
12
17
|
alias: 'config',
|
|
13
18
|
describe: 'Path to the configuration file',
|
|
14
19
|
demandOption: false,
|
|
15
|
-
type: 'string'
|
|
16
|
-
|
|
20
|
+
type: 'string'
|
|
21
|
+
},
|
|
22
|
+
q: {
|
|
23
|
+
alias: 'quality',
|
|
24
|
+
describe: 'Image quality',
|
|
25
|
+
type: 'number'
|
|
26
|
+
},
|
|
27
|
+
l: {
|
|
28
|
+
alias: 'compressionLevel',
|
|
29
|
+
describe: 'PNG compression level',
|
|
30
|
+
type: 'number'
|
|
31
|
+
},
|
|
32
|
+
w: {
|
|
33
|
+
alias: 'maxWidth',
|
|
34
|
+
describe: 'Maximum width allowed',
|
|
35
|
+
type: 'number'
|
|
36
|
+
},
|
|
37
|
+
h: {
|
|
38
|
+
alias: 'maxHeight',
|
|
39
|
+
describe: 'Maximum height allowed',
|
|
40
|
+
type: 'number'
|
|
41
|
+
},
|
|
42
|
+
i: {
|
|
43
|
+
alias: 'input',
|
|
44
|
+
describe: 'Path to the input directory',
|
|
45
|
+
type: 'string'
|
|
46
|
+
},
|
|
47
|
+
o: {
|
|
48
|
+
alias: 'output',
|
|
49
|
+
describe: 'Path to the output directory',
|
|
50
|
+
type: 'string'
|
|
51
|
+
},
|
|
52
|
+
f: {
|
|
53
|
+
alias: 'formats',
|
|
54
|
+
describe: 'Output formats (comma separated). Default: source. Options: source, webp, avif',
|
|
55
|
+
type: 'string'
|
|
17
56
|
}
|
|
18
57
|
}).argv
|
|
19
58
|
}
|
|
20
59
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
* @param {string} dir - The directory to verify.
|
|
25
|
-
*/
|
|
26
|
-
async function createOutputDirectory (dir) {
|
|
27
|
-
try {
|
|
28
|
-
await fs.access(dir)
|
|
29
|
-
} catch (error) {
|
|
30
|
-
await fs.mkdir(dir)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Process and optimize an image file.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} inputPath - Path to the input image file.
|
|
38
|
-
* @param {string} outputPath - Path to the output image file.
|
|
39
|
-
* @param {string} file - The file name.
|
|
40
|
-
* @param {number} maxWidth - Maximum width allowed.
|
|
41
|
-
* @param {number} maxHeight - Maximum height allowed.
|
|
42
|
-
* @param {number} quality - Image quality.
|
|
43
|
-
* @param {number} compressionLevel - PNG compression level.
|
|
44
|
-
*/
|
|
45
|
-
async function processImage (inputPath, outputPath, file, maxWidth, maxHeight, quality, compressionLevel) {
|
|
46
|
-
try {
|
|
47
|
-
const image = sharp(inputPath)
|
|
48
|
-
const metadata = await image.metadata()
|
|
49
|
-
const originalWidth = metadata.width
|
|
50
|
-
const originalHeight = metadata.height
|
|
51
|
-
const imageProcessor = image.resize(
|
|
52
|
-
originalWidth > maxWidth ? maxWidth : null,
|
|
53
|
-
originalHeight > maxHeight ? maxHeight : null
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
if (file.match(/\.(jpg|jpeg)$/i)) {
|
|
57
|
-
await imageProcessor.jpeg({ quality }).toFile(outputPath)
|
|
58
|
-
console.log(`Optimized JPEG image: ${file}`)
|
|
59
|
-
} else if (file.match(/\.(png)$/i)) {
|
|
60
|
-
await imageProcessor.png({ quality, compressionLevel }).toFile(outputPath)
|
|
61
|
-
console.log(`Optimized PNG image: ${file}`)
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.error(`Optimization error ${file}: `, error)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
60
|
+
async function main () {
|
|
61
|
+
console.log(chalk.blue('picslim - Image Optimization Tool'))
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
* Load settings from a configuration file.
|
|
70
|
-
*
|
|
71
|
-
* @param {string} configFile - Path to the configuration file.
|
|
72
|
-
*/
|
|
73
|
-
async function loadConfig (configFile) {
|
|
74
|
-
try {
|
|
75
|
-
const configData = await fs.readFile(configFile, 'utf8')
|
|
63
|
+
const argv = parseArguments()
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
process.exit(1)
|
|
80
|
-
}
|
|
65
|
+
// Load config (merges defaults, config file, and argv)
|
|
66
|
+
const baseConfig = await loadConfig(argv.config)
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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())
|
|
72
|
+
}
|
|
86
73
|
|
|
87
|
-
|
|
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
|
|
84
|
+
}
|
|
88
85
|
|
|
89
|
-
|
|
86
|
+
const inputDir = path.resolve(config.inputDir)
|
|
87
|
+
const outputDir = path.resolve(config.outputDir)
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
process.exit(1)
|
|
94
|
-
}
|
|
89
|
+
console.log(chalk.gray(`Input: ${inputDir}`))
|
|
90
|
+
console.log(chalk.gray(`Output: ${outputDir}`))
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.error('Error loading or parsing the configuration file: ', error)
|
|
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}`))
|
|
104
99
|
process.exit(1)
|
|
105
100
|
}
|
|
106
|
-
}
|
|
107
101
|
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
try {
|
|
111
|
-
JSON.parse(str)
|
|
112
|
-
} catch (e) {
|
|
113
|
-
// Error
|
|
114
|
-
// JSON is not okay
|
|
115
|
-
return false
|
|
116
|
-
}
|
|
102
|
+
// Filter images
|
|
103
|
+
const images = files.filter(isImage)
|
|
117
104
|
|
|
118
|
-
|
|
105
|
+
if (images.length === 0) {
|
|
106
|
+
console.log(chalk.yellow('No images found to process.'))
|
|
107
|
+
return
|
|
119
108
|
}
|
|
120
109
|
|
|
121
|
-
|
|
122
|
-
}
|
|
110
|
+
console.log(chalk.green(`Found ${images.length} images. Starting optimization...`))
|
|
123
111
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* @param {object} config - The loaded configuration object.
|
|
128
|
-
*/
|
|
129
|
-
function validateConfig (config) {
|
|
130
|
-
if (!config.inputDir || !config.outputDir) {
|
|
131
|
-
console.error("Configuration error: 'inputDir' and 'outputDir' are required fields.")
|
|
132
|
-
process.exit(1)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
112
|
+
// Prepare batch processing
|
|
113
|
+
const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic)
|
|
114
|
+
progressBar.start(images.length, 0)
|
|
135
115
|
|
|
136
|
-
|
|
137
|
-
* The main function that processes all image files in the input directory.
|
|
138
|
-
*/
|
|
139
|
-
async function main () {
|
|
140
|
-
const argv = parseArguments()
|
|
141
|
-
const config = await loadConfig(argv.config)
|
|
116
|
+
const concurrencyLevel = os.cpus().length // Use number of CPU cores
|
|
142
117
|
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const quality = config.quality
|
|
146
|
-
const maxWidth = config.maxWidth
|
|
147
|
-
const maxHeight = config.maxHeight
|
|
148
|
-
const compressionLevel = config.compressionLevel
|
|
118
|
+
let completed = 0
|
|
119
|
+
const errors = []
|
|
149
120
|
|
|
150
|
-
|
|
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
|
+
)
|
|
151
125
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
await processImage(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
126
|
+
const batches = chunk(images, concurrencyLevel)
|
|
127
|
+
|
|
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.'))
|
|
161
151
|
}
|
|
152
|
+
console.log(chalk.blue('--------------------------------------------------'))
|
|
162
153
|
}
|
|
163
154
|
|
|
164
155
|
main()
|