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.
Files changed (3) hide show
  1. package/README.md +60 -32
  2. package/package.json +8 -2
  3. 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 allows you to optimize images in a specified directory. It supports JPEG and PNG images, and you can control the quality and resizing of the images during optimization.
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
- # Usage
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
- **-q, --quality [value]:** Set the image quality (1 to 100, default: 80).<br>
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
- Example:
27
+ Once installed, you can use the `picslim` command in your terminal:
29
28
 
30
29
  ```bash
31
- picslim -q 90 -w 1920
30
+ picslim [options]
32
31
  ```
33
32
 
34
- This will optimize all JPEG and PNG images in the current directory, and the optimized images will be saved in a 'min' directory.
35
-
36
- ### Example
37
-
38
- Suppose you have a directory with the following images:
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
- - image1.jpg
41
- - image2.jpg
42
- - image3.png
43
- - image4.png
60
+ ### Examples
44
61
 
45
- You can optimize all these images with the following command:
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 -q 90 -w 1920
70
+ picslim -i ./assets -o ./build/assets -w 800
49
71
  ```
50
72
 
51
- After running the command, you will have the following directory structure:
73
+ **3. Generate WebP Only**
74
+ Converts all images to WebP format.
75
+ ```bash
76
+ picslim -f webp
77
+ ```
52
78
 
53
- ```markdown
54
- - min
55
- - image1.jpg
56
- - image2.jpg
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.8",
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 -q 80",
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
- default: 'config.json'
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
- * Verifies if the output directory exists; if not, creates it.
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
- if (configData.trim() === '') {
78
- console.error('Configuration file is empty.')
79
- process.exit(1)
80
- }
65
+ // Load config (merges defaults, config file, and argv)
66
+ const baseConfig = await loadConfig(argv.config)
81
67
 
82
- if (!validateJson(configData)) {
83
- console.error('Configuration file is not a valid JSON object.')
84
- process.exit(1)
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
- const config = JSON.parse(configData)
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
- validateConfig(config)
86
+ const inputDir = path.resolve(config.inputDir)
87
+ const outputDir = path.resolve(config.outputDir)
90
88
 
91
- if (typeof config !== 'object' || Array.isArray(config)) {
92
- console.error('Configuration file is not a valid JSON object.')
93
- process.exit(1)
94
- }
89
+ console.log(chalk.gray(`Input: ${inputDir}`))
90
+ console.log(chalk.gray(`Output: ${outputDir}`))
95
91
 
96
- if (!config.inputDir || !config.outputDir) {
97
- console.error("Configuration error: 'inputDir' and 'outputDir' are required fields.")
98
- process.exit(1)
99
- }
100
-
101
- return config
102
- } catch (error) {
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
- function validateJson (json) {
109
- const isJson = (str) => {
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
- return true
105
+ if (images.length === 0) {
106
+ console.log(chalk.yellow('No images found to process.'))
107
+ return
119
108
  }
120
109
 
121
- return isJson(json)
122
- }
110
+ console.log(chalk.green(`Found ${images.length} images. Starting optimization...`))
123
111
 
124
- /**
125
- * Validate the configuration object.
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
- const inputDir = config.inputDir
144
- const outputDir = config.outputDir
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
- await createOutputDirectory(outputDir)
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
- try {
153
- const files = await fs.readdir(inputDir)
154
- for (const file of files) {
155
- const inputPath = `${inputDir}/${file}`
156
- const outputPath = `${outputDir}/${file}`
157
- await processImage(inputPath, outputPath, file, maxWidth, maxHeight, quality, compressionLevel)
158
- }
159
- } catch (error) {
160
- console.error('Error reading input directory: ', error)
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()