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.
Files changed (3) hide show
  1. package/README.md +26 -34
  2. package/package.json +8 -2
  3. 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 allows you to efficiently optimize images within a specified directory. It supports **JPEG** and **PNG** images, image formats, offering fine-grained control over image quality and resizing options during the optimization process. With **Picslim**, you can effortlessly reduce file sizes and enhance the loading performance of your images.
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 optimizimage command in your terminal. Here's how you can use it:
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. Here's an example configuration:
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
- ### Example:
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
- Optimize images with custom settings:
61
-
68
+ **2. Custom Input/Output and Resizing**
62
69
  ```bash
63
- picslim -q 90 -w 800 -h 600 -l 4 -i input_images -o output_images
70
+ picslim -i ./assets -o ./build/assets -w 800
64
71
  ```
65
72
 
66
- This will optimize all JPEG and PNG images in the current directory, and the optimized images will be saved in a 'min' directory.
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 -q 90 -w 1920
76
+ picslim -f webp
81
77
  ```
82
78
 
83
- After running the command, you will have the following directory structure:
84
-
85
- ```markdown
86
- - min
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": "1.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 -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,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
- demandOption: false,
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
- demandOption: false,
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
- demandOption: false,
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
- demandOption: false,
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
- demandOption: false,
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
- demandOption: false,
57
- type: 'string',
58
- default: null
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
- * Creates an output directory if it doesn't exist.
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
- * Process and optimize an image file.
78
- *
79
- * @param {string} inputPath - Path to the input image file.
80
- * @param {string} outputPath - Path to the output image file.
81
- * @param {string} file - The file name.
82
- * @param {number} maxWidth - Maximum width allowed.
83
- * @param {number} maxHeight - Maximum height allowed.
84
- * @param {number} quality - Image quality.
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
- * Load settings from a configuration file. If the specified configuration file doesn't exist,
112
- * it will use the default 'config.json' from the package.
113
- *
114
- * @param {string} configFile - Path to the configuration file.
115
- */
116
- async function loadConfig (configFile) {
117
- let config
118
- try {
119
- const configData = await fs.readFile(configFile, 'utf8')
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
- if (!validateConfigObject(config)) {
127
- console.error('Invalid configuration object.')
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
- if (!config.inputDir || !config.outputDir) {
132
- console.error("Configuration error: 'inputDir' and 'outputDir' are required fields.")
133
- process.exit(1)
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
- return config
137
- }
110
+ console.log(chalk.green(`Found ${images.length} images. Starting optimization...`))
138
111
 
139
- /**
140
- * Validate the configuration object.
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
- return argvInput || configInputDir
166
- }
118
+ let completed = 0
119
+ const errors = []
167
120
 
168
- /**
169
- * The main function that processes all image files in the input directory.
170
- */
171
- async function main () {
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
- await createOutputDirectory(outputDir)
126
+ const batches = chunk(images, concurrencyLevel)
182
127
 
183
- try {
184
- const files = await fs.readdir(inputDir)
185
- for (const file of files) {
186
- const inputPath = `${inputDir}/${file}`
187
- const outputPath = `${outputDir}/${file}`
188
- await processImage(inputPath, outputPath, file, maxWidth, maxHeight, quality, compressionLevel)
189
- }
190
- } catch (error) {
191
- console.error('Error reading input directory: ', error)
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()