auto-image-converter 2.1.1 → 2.2.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 +154 -28
- package/bin/index.js +50 -19
- package/bin/onetimeresizer.mjs +93 -0
- package/bin/watcher.mjs +52 -35
- package/image-converter.config.mjs +19 -7
- package/lib/ConvertImages.js +68 -0
- package/lib/CreateSrcSet.js +60 -0
- package/lib/FileManager.js +205 -0
- package/lib/MarkerFile.js +98 -0
- package/lib/Pipeline.js +378 -0
- package/lib/Queue.js +41 -0
- package/lib/ResizeImages.js +105 -0
- package/lib/converter.js +135 -50
- package/lib/pipelines/ConvertationPipeline.js +182 -0
- package/lib/steps/ConvertationStep.js +493 -0
- package/package.json +29 -27
package/README.md
CHANGED
|
@@ -1,69 +1,195 @@
|
|
|
1
1
|
# 🖼️ Auto Image Converter
|
|
2
2
|
|
|
3
|
-
Automatically convert
|
|
3
|
+
Automatically convert and resize images to modern formats (WebP, AVIF, PNG, JPEG, TIFF) with real-time file watching and flexible resize options.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 🔗 GitHub repository
|
|
6
|
+
|
|
7
|
+
<https://github.com/StoneZol/auto-image-converter>
|
|
6
8
|
|
|
7
9
|
## 🚀 Features
|
|
8
10
|
|
|
9
|
-
🔄 Convert images to
|
|
11
|
+
🔄 **Convert images** to WebP, AVIF, PNG, JPEG, or TIFF
|
|
12
|
+
|
|
13
|
+
📐 **Resize images** by width, height, or to specific aspect ratios
|
|
14
|
+
|
|
15
|
+
🔍 **Recursive folder support** - process entire directory trees
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
🗑️ **Optional removal** of original files after conversion
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
👀 **Watch mode** – automatically converts newly added images in real-time
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
⚡ **Parallel processing** with configurable concurrency
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
🎯 **One-time resize** – resize already converted images without re-conversion
|
|
24
|
+
|
|
25
|
+
⚙️ **Easy configuration** via `image-converter.config.mjs`
|
|
18
26
|
|
|
19
27
|
## 📦 Installation
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
```bash
|
|
30
|
+
npm install auto-image-converter --save-dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or use locally via `npm link`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd auto-image-converter
|
|
37
|
+
npm link
|
|
38
|
+
cd ../your-project
|
|
39
|
+
npm link auto-image-converter
|
|
40
|
+
```
|
|
22
41
|
|
|
23
42
|
## ⚙️ Configuration
|
|
24
43
|
|
|
25
44
|
Create a `image-converter.config.mjs` file in the root of your project:
|
|
26
45
|
|
|
27
|
-
|
|
46
|
+
```javascript
|
|
28
47
|
export default {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
// Main settings
|
|
49
|
+
dir: "./public", // Directory to scan for images
|
|
50
|
+
removeOriginal: true, // Delete original files after conversion
|
|
51
|
+
recursive: true, // Recursive search in subdirectories
|
|
52
|
+
ignoreOnStart: true, // Ignore existing files on watcher startup
|
|
53
|
+
concurrency: 4, // Number of parallel workers
|
|
54
|
+
|
|
55
|
+
// Conversion settings
|
|
56
|
+
convertation: {
|
|
57
|
+
converted: "*.{png,jpg,jpeg,tiff}", // Source file pattern
|
|
58
|
+
format: "webp", // Target format: webp, avif, png, jpg, tiff
|
|
59
|
+
quality: 80, // Quality (0-100)
|
|
60
|
+
outputDir: null, // null = same folder, or path for output
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Resize settings (optional)
|
|
64
|
+
needResize: true, // Enable resize
|
|
65
|
+
resize: {
|
|
66
|
+
width: 1920, // Width (or null)
|
|
67
|
+
height: null, // Height (or null)
|
|
68
|
+
fit: "cover", // cover, contain, fill, inside, outside
|
|
69
|
+
position: "center", // Cropping position
|
|
70
|
+
withoutEnlargement: true, // Don't enlarge small images
|
|
71
|
+
},
|
|
36
72
|
};
|
|
37
73
|
```
|
|
38
74
|
|
|
75
|
+
### Resize Options
|
|
76
|
+
|
|
77
|
+
- **By width only**: `width: 1920, height: null` - reduces to width, height scales proportionally
|
|
78
|
+
- **By height only**: `width: null, height: 1080` - reduces to height, width scales proportionally
|
|
79
|
+
- **To aspect ratio**: `width: 1920, height: 1080` - fits/crops to specific aspect ratio
|
|
80
|
+
|
|
81
|
+
### Fit Modes
|
|
82
|
+
|
|
83
|
+
- `cover` - fills entire size, cropping excess (default)
|
|
84
|
+
- `contain` - fits fully, may add padding
|
|
85
|
+
- `fill` - stretches without preserving aspect ratio
|
|
86
|
+
- `inside` - reduces to fit, doesn't enlarge
|
|
87
|
+
- `outside` - covers entire size, may enlarge
|
|
88
|
+
|
|
39
89
|
## 🛠️ Usage
|
|
40
90
|
|
|
41
|
-
|
|
91
|
+
### Commands
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# One-time conversion of all files
|
|
95
|
+
npx auto-convert-images
|
|
96
|
+
|
|
97
|
+
# Watch mode - processes new files as they're added
|
|
98
|
+
npx auto-convert-images-watch
|
|
99
|
+
|
|
100
|
+
# One-time resize of already converted files
|
|
101
|
+
npx auto-convert-images-resize
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Package.json Scripts
|
|
105
|
+
|
|
106
|
+
Add to your `package.json`:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"scripts": {
|
|
111
|
+
"convert": "auto-convert-images",
|
|
112
|
+
"watch": "auto-convert-images-watch",
|
|
113
|
+
"resize": "auto-convert-images-resize"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then run:
|
|
119
|
+
|
|
120
|
+
- `npm run convert` - one-time conversion
|
|
121
|
+
- `npm run watch` - watch mode
|
|
122
|
+
- `npm run resize` - resize already converted files
|
|
123
|
+
|
|
124
|
+
### With Next.js
|
|
125
|
+
|
|
126
|
+
To run the watcher alongside the development server:
|
|
42
127
|
|
|
128
|
+
```bash
|
|
129
|
+
npm install concurrently --save-dev
|
|
43
130
|
```
|
|
44
|
-
|
|
45
|
-
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"scripts": {
|
|
135
|
+
"dev": "concurrently \"npm run watch\" \"next dev\""
|
|
136
|
+
}
|
|
46
137
|
}
|
|
47
138
|
```
|
|
48
139
|
|
|
49
|
-
|
|
140
|
+
## 📋 Use Cases
|
|
50
141
|
|
|
51
|
-
|
|
142
|
+
### 1. Initial Conversion
|
|
52
143
|
|
|
53
|
-
|
|
144
|
+
Convert all PNG/JPG files to WebP:
|
|
54
145
|
|
|
55
|
-
|
|
146
|
+
```bash
|
|
147
|
+
npx auto-convert-images
|
|
148
|
+
```
|
|
56
149
|
|
|
57
|
-
|
|
150
|
+
### 2. Watch Mode
|
|
58
151
|
|
|
59
|
-
|
|
152
|
+
Automatically convert new images as they're added:
|
|
60
153
|
|
|
154
|
+
```bash
|
|
155
|
+
npx auto-convert-images-watch
|
|
61
156
|
```
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
157
|
+
|
|
158
|
+
### 3. Resize Already Converted Files
|
|
159
|
+
|
|
160
|
+
If you converted files without resize, then decided you need resize:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npx auto-convert-images-resize
|
|
65
164
|
```
|
|
66
165
|
|
|
166
|
+
- If `removeOriginal: false` → creates new files with size suffix: `image.webp` → `image-1920x1080.webp`
|
|
167
|
+
- If `removeOriginal: true` → overwrites original files
|
|
168
|
+
|
|
169
|
+
## 🏗️ Architecture
|
|
170
|
+
|
|
171
|
+
The tool uses a modular architecture:
|
|
172
|
+
|
|
173
|
+
- **Pipeline** - main processing engine with queue and workers
|
|
174
|
+
- **ResizeImages** - resize operations wrapper
|
|
175
|
+
- **ConvertImages** - format conversion wrapper
|
|
176
|
+
- **FileManager** - file path resolution and saving
|
|
177
|
+
|
|
178
|
+
All operations work with Sharp instances in a chainable pipeline pattern.
|
|
179
|
+
|
|
180
|
+
## 🐛 Bug Reports
|
|
181
|
+
|
|
182
|
+
Found a bug? Please report it in the [GitHub Issues](https://github.com/StoneZol/auto-image-converter/issues) section of the repository. Include:
|
|
183
|
+
|
|
184
|
+
- Description of the issue
|
|
185
|
+
- Steps to reproduce
|
|
186
|
+
- Expected behavior
|
|
187
|
+
- Actual behavior
|
|
188
|
+
- Configuration file (if relevant)
|
|
189
|
+
- Node.js version and OS
|
|
190
|
+
|
|
191
|
+
This helps improve the tool for everyone!
|
|
192
|
+
|
|
67
193
|
## 📄 License
|
|
68
194
|
|
|
69
195
|
This project is open source and available under the MIT License.
|
package/bin/index.js
CHANGED
|
@@ -1,28 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { Pipeline } from "../lib/Pipeline.js";
|
|
5
6
|
|
|
6
|
-
const CONFIG_PATH = path.resolve(
|
|
7
|
+
const CONFIG_PATH = path.resolve(
|
|
8
|
+
process.cwd(),
|
|
9
|
+
"image-converter.config.mjs"
|
|
10
|
+
);
|
|
7
11
|
|
|
8
12
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
// Проверяем существование конфига
|
|
14
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
15
|
+
console.error(
|
|
16
|
+
`❌ Config file not found: ${CONFIG_PATH}\n` +
|
|
17
|
+
` Please create 'image-converter.config.mjs' in the current directory.`
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
11
21
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
const configModule = await import(
|
|
23
|
+
pathToFileURL(CONFIG_PATH).href
|
|
24
|
+
);
|
|
25
|
+
const config = configModule.default;
|
|
26
|
+
|
|
27
|
+
if (!config) {
|
|
28
|
+
console.error(
|
|
29
|
+
`❌ Config file is empty or doesn't export default config.\n` +
|
|
30
|
+
` File: ${CONFIG_PATH}`
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pipeline = new Pipeline(config);
|
|
36
|
+
const stats = await pipeline.run();
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
recursive: config.recursive ?? true,
|
|
23
|
-
removeOriginal: config.removeOriginal ?? false,
|
|
24
|
-
});
|
|
38
|
+
console.log(`\n✅ Processing complete:`);
|
|
39
|
+
console.log(` Total: ${stats.total}`);
|
|
40
|
+
console.log(` Converted: ${stats.converted}`);
|
|
41
|
+
console.log(` Skipped: ${stats.skipped}`);
|
|
42
|
+
console.log(` Failed: ${stats.failed}`);
|
|
25
43
|
} catch (e) {
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" && e.message.includes("image-converter.config.mjs")) {
|
|
45
|
+
console.error(
|
|
46
|
+
`❌ Config file not found: ${CONFIG_PATH}\n` +
|
|
47
|
+
` Please create 'image-converter.config.mjs' in the current directory.`
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
console.error(
|
|
51
|
+
"❌ Error:",
|
|
52
|
+
e.message
|
|
53
|
+
);
|
|
54
|
+
if (e.stack) {
|
|
55
|
+
console.error(e.stack);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
process.exit(1);
|
|
28
59
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { Pipeline } from "../lib/Pipeline.js";
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = path.resolve(
|
|
8
|
+
process.cwd(),
|
|
9
|
+
"image-converter.config.mjs"
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Проверяем существование конфига
|
|
14
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
15
|
+
console.error(
|
|
16
|
+
`❌ Config file not found: ${CONFIG_PATH}\n` +
|
|
17
|
+
` Please create 'image-converter.config.mjs' in the current directory.`
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const configModule = await import(
|
|
23
|
+
pathToFileURL(CONFIG_PATH).href
|
|
24
|
+
);
|
|
25
|
+
const originalConfig = configModule.default;
|
|
26
|
+
|
|
27
|
+
if (!originalConfig) {
|
|
28
|
+
console.error(
|
|
29
|
+
`❌ Config file is empty or doesn't export default config.\n` +
|
|
30
|
+
` File: ${CONFIG_PATH}`
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Проверяем, что в конфиге есть resize настройки
|
|
36
|
+
if (!originalConfig.resize) {
|
|
37
|
+
console.error(
|
|
38
|
+
`❌ Resize config is missing.\n` +
|
|
39
|
+
` Please add 'resize' section to your config.`
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Создаём модифицированный конфиг для ресайза уже сконвертированных файлов
|
|
45
|
+
const targetFormat = (
|
|
46
|
+
originalConfig.convertation?.format ?? originalConfig.format ?? "webp"
|
|
47
|
+
).toLowerCase();
|
|
48
|
+
|
|
49
|
+
const resizeConfig = {
|
|
50
|
+
...originalConfig,
|
|
51
|
+
// Ищем файлы в целевом формате (уже сконвертированные)
|
|
52
|
+
convertation: {
|
|
53
|
+
...originalConfig.convertation,
|
|
54
|
+
converted: `*.${targetFormat}`, // только файлы в целевом формате (без фигурных скобок для одного формата)
|
|
55
|
+
format: targetFormat, // оставляем тот же формат (не конвертируем)
|
|
56
|
+
},
|
|
57
|
+
// Принудительно включаем ресайз
|
|
58
|
+
needResize: true,
|
|
59
|
+
resize: originalConfig.resize,
|
|
60
|
+
// Используем removeOriginal из конфига
|
|
61
|
+
removeOriginal: originalConfig.removeOriginal ?? false,
|
|
62
|
+
// Флаг для режима ресайза (чтобы Pipeline знал, что нужно добавлять размер в имя)
|
|
63
|
+
isResizeMode: true,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
console.log(`🔄 Resizing already converted ${targetFormat.toUpperCase()} files...`);
|
|
67
|
+
console.log(` Resize config: ${JSON.stringify(originalConfig.resize, null, 2)}\n`);
|
|
68
|
+
|
|
69
|
+
const pipeline = new Pipeline(resizeConfig);
|
|
70
|
+
const stats = await pipeline.run();
|
|
71
|
+
|
|
72
|
+
console.log(`\n✅ Resize complete:`);
|
|
73
|
+
console.log(` Total: ${stats.total}`);
|
|
74
|
+
console.log(` Resized: ${stats.converted}`);
|
|
75
|
+
console.log(` Skipped: ${stats.skipped}`);
|
|
76
|
+
console.log(` Failed: ${stats.failed}`);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" && e.message.includes("image-converter.config.mjs")) {
|
|
79
|
+
console.error(
|
|
80
|
+
`❌ Config file not found: ${CONFIG_PATH}\n` +
|
|
81
|
+
` Please create 'image-converter.config.mjs' in the current directory.`
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
console.error(
|
|
85
|
+
"❌ Error:",
|
|
86
|
+
e.message
|
|
87
|
+
);
|
|
88
|
+
if (e.stack) {
|
|
89
|
+
console.error(e.stack);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
package/bin/watcher.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chokidar from "chokidar";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { Pipeline } from "../lib/Pipeline.js";
|
|
5
5
|
import { pathToFileURL } from "url";
|
|
6
6
|
|
|
7
7
|
const configPath = path.resolve(process.cwd(), "image-converter.config.mjs");
|
|
@@ -9,48 +9,65 @@ const config = (await import(pathToFileURL(configPath).href)).default;
|
|
|
9
9
|
|
|
10
10
|
const watchDir = config.dir || "public";
|
|
11
11
|
const absWatchDir = path.resolve(process.cwd(), watchDir);
|
|
12
|
-
const extensions = extractExtensions(
|
|
12
|
+
const extensions = extractExtensions(
|
|
13
|
+
config.convertation?.converted ?? config.converted ?? "*.{png,jpg,jpeg}"
|
|
14
|
+
);
|
|
15
|
+
const targetFormat = (
|
|
16
|
+
config.convertation?.format ?? config.format ?? "webp"
|
|
17
|
+
).toLowerCase();
|
|
13
18
|
|
|
14
19
|
const watchPath = absWatchDir;
|
|
15
20
|
|
|
16
21
|
console.log(`👀 Watching for image changes on directory: ${watchPath}`);
|
|
17
22
|
|
|
23
|
+
// Создаём Pipeline и запускаем воркеров в постоянном режиме
|
|
24
|
+
const pipeline = new Pipeline(config);
|
|
25
|
+
pipeline.startWorkers();
|
|
26
|
+
|
|
18
27
|
let debounceTimeout;
|
|
28
|
+
let pendingFiles = new Set();
|
|
19
29
|
|
|
20
30
|
chokidar
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
.watch(absWatchDir, {
|
|
32
|
+
ignored: /(^|[\/\\])\../,
|
|
33
|
+
persistent: true,
|
|
34
|
+
ignoreInitial: config.ignoreOnStart ?? false,
|
|
35
|
+
awaitWriteFinish: {
|
|
36
|
+
stabilityThreshold: 1500,
|
|
37
|
+
pollInterval: 500,
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
.on("add", async (filePath) => {
|
|
41
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
42
|
+
if (!extensions.includes(ext)) return;
|
|
43
|
+
if (ext === targetFormat) return;
|
|
44
|
+
|
|
45
|
+
console.log(`➕ New image: ${filePath}`);
|
|
46
|
+
pendingFiles.add(filePath);
|
|
47
|
+
|
|
48
|
+
clearTimeout(debounceTimeout);
|
|
49
|
+
debounceTimeout = setTimeout(async () => {
|
|
50
|
+
// Обрабатываем все накопленные файлы
|
|
51
|
+
const filesToProcess = Array.from(pendingFiles);
|
|
52
|
+
pendingFiles.clear();
|
|
53
|
+
|
|
54
|
+
for (const file of filesToProcess) {
|
|
55
|
+
pipeline.enqueueFile(file);
|
|
56
|
+
}
|
|
57
|
+
// Воркеры уже работают, просто добавляем задачи в очередь
|
|
58
|
+
}, 1000);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Graceful shutdown
|
|
62
|
+
process.on("SIGINT", async () => {
|
|
63
|
+
console.log("\n🛑 Stopping watcher...");
|
|
64
|
+
await pipeline.stop();
|
|
65
|
+
process.exit(0);
|
|
66
|
+
});
|
|
52
67
|
|
|
53
68
|
function extractExtensions(pattern) {
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
const match = pattern.match(/\*\.\{(.+?)\}/);
|
|
70
|
+
return match
|
|
71
|
+
? match[1].split(",").map((s) => s.trim().toLowerCase())
|
|
72
|
+
: [];
|
|
56
73
|
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
// Основные настройки
|
|
3
|
+
dir: "./public",
|
|
4
|
+
removeOriginal: true,
|
|
5
|
+
recursive: true,
|
|
6
|
+
ignoreOnStart: true,
|
|
7
|
+
concurrency: 4,
|
|
8
|
+
convertation: {
|
|
9
|
+
converted: "*.{png,jpg,jpeg,tiff}", // png, jpg, jpeg, tiff, webp, avif
|
|
10
|
+
format: "webp", // png, jpeg, tiff, webp, avif
|
|
11
|
+
quality: 80, // 0-100
|
|
12
|
+
},
|
|
13
|
+
needResize: true, // true or false
|
|
14
|
+
resize: {
|
|
15
|
+
width: 1920, // 100-1000 or null
|
|
16
|
+
height: null, // 100-1000 or null
|
|
17
|
+
fit: "cover", // cover, contain, fill, inside, outside
|
|
18
|
+
position: "center", // center, top, bottom, left, right, top-left, top-right, bottom-left, bottom-right
|
|
19
|
+
withoutEnlargement: true, // true or false
|
|
20
|
+
},
|
|
9
21
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Абстракция для конвертации форматов.
|
|
5
|
+
*
|
|
6
|
+
* Главное правило:
|
|
7
|
+
* - на вход можно дать путь/Buffer ИЛИ уже готовый sharp‑инстанс;
|
|
8
|
+
* - методы конверсии возвращают sharp‑инстанс;
|
|
9
|
+
* - .toBuffer() / .toFile() вызываются уже снаружи (в пайплайне).
|
|
10
|
+
*/
|
|
11
|
+
export class ConvertImages {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string|Buffer|import("sharp").Sharp} input
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @param {number} options.quality
|
|
16
|
+
*/
|
|
17
|
+
constructor(input, options) {
|
|
18
|
+
this.quality = options.quality;
|
|
19
|
+
|
|
20
|
+
// Если нам уже дали sharp‑инстанс — просто переиспользуем его.
|
|
21
|
+
if (input && typeof input === "object" && typeof input.resize === "function") {
|
|
22
|
+
this.instance = input;
|
|
23
|
+
} else {
|
|
24
|
+
// Иначе считаем, что это путь или Buffer.
|
|
25
|
+
this.instance = sharp(input);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Конвертировать в WebP.
|
|
31
|
+
* @returns {import("sharp").Sharp}
|
|
32
|
+
*/
|
|
33
|
+
toWebp() {
|
|
34
|
+
return this.instance.webp({ quality: this.quality });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Конвертировать в AVIF.
|
|
39
|
+
* @returns {import("sharp").Sharp}
|
|
40
|
+
*/
|
|
41
|
+
toAvif() {
|
|
42
|
+
return this.instance.avif({ quality: this.quality });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Конвертировать в PNG.
|
|
47
|
+
* @returns {import("sharp").Sharp}
|
|
48
|
+
*/
|
|
49
|
+
toPng() {
|
|
50
|
+
return this.instance.png({ quality: this.quality });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Конвертировать в JPEG.
|
|
55
|
+
* @returns {import("sharp").Sharp}
|
|
56
|
+
*/
|
|
57
|
+
toJpg() {
|
|
58
|
+
return this.instance.jpeg({ quality: this.quality });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Конвертировать в TIFF.
|
|
63
|
+
* @returns {import("sharp").Sharp}
|
|
64
|
+
*/
|
|
65
|
+
toTiff() {
|
|
66
|
+
return this.instance.tiff({ quality: this.quality });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export class CreateSrcSet {
|
|
2
|
+
// Resize modes
|
|
3
|
+
static RESIZE_MODES = {
|
|
4
|
+
COVER: "cover", // Fill the entire size, cropping excess
|
|
5
|
+
CONTAIN: "contain", // Contain fully, preserving proportions
|
|
6
|
+
FILL: "fill", // Stretch to size (distortion)
|
|
7
|
+
INSIDE: "inside", // Contain inside size (like contain)
|
|
8
|
+
OUTSIDE: "outside", // Cover outside (like cover)
|
|
9
|
+
};
|
|
10
|
+
constructor(filePath, options = {}) {
|
|
11
|
+
this.filePath = filePath;
|
|
12
|
+
this.format = options.format;
|
|
13
|
+
this.quality = options.quality;
|
|
14
|
+
this.resizeMode = options.resizeMode; // [{w:32,h:32}, {w:64,h:64}, {w:128,h:128}]
|
|
15
|
+
this.w = options.w;
|
|
16
|
+
this.h = options.h;
|
|
17
|
+
}
|
|
18
|
+
getOutputFileName(w, h) {
|
|
19
|
+
return `${this.nameWithoutExt}-${w}x${h}.${this.format}`;
|
|
20
|
+
}
|
|
21
|
+
createCover(w, h) {
|
|
22
|
+
return sharp(this.filePath).resize(w, h).toBuffer();
|
|
23
|
+
}
|
|
24
|
+
createContain(w, h) {
|
|
25
|
+
return sharp(this.filePath).resize(w, h).toBuffer();
|
|
26
|
+
}
|
|
27
|
+
createFill(w, h) {
|
|
28
|
+
return sharp(this.filePath).resize(w, h).toBuffer();
|
|
29
|
+
}
|
|
30
|
+
createInside(w, h) {
|
|
31
|
+
return sharp(this.filePath).resize(w, h).toBuffer();
|
|
32
|
+
}
|
|
33
|
+
createOutside(w, h) {
|
|
34
|
+
return sharp(this.filePath).resize(w, h).toBuffer();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
create(w, h) {
|
|
38
|
+
switch (this.resizeMode) {
|
|
39
|
+
case this.RESIZE_MODES.COVER:
|
|
40
|
+
return this.createCover(w, h);
|
|
41
|
+
case this.RESIZE_MODES.CONTAIN:
|
|
42
|
+
return this.createContain(w, h);
|
|
43
|
+
case this.RESIZE_MODES.FILL:
|
|
44
|
+
return this.createFill(w, h);
|
|
45
|
+
case this.RESIZE_MODES.INSIDE:
|
|
46
|
+
return this.createInside(w, h);
|
|
47
|
+
case this.RESIZE_MODES.OUTSIDE:
|
|
48
|
+
return this.createOutside(w, h);
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid resize mode: ${this.resizeMode}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
createSrcSet() {
|
|
56
|
+
return this.resizeModes.map((mode) => {
|
|
57
|
+
return this.create(mode.w, mode.h);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|