auto-image-converter 2.1.0 → 2.1.2
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/bin/index.js +25 -17
- package/bin/watcher.mjs +1 -1
- package/image-converter.config.mjs +1 -1
- package/lib/converter.js +135 -50
- package/package.json +26 -26
package/bin/index.js
CHANGED
|
@@ -3,26 +3,34 @@ import path from "path";
|
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
4
4
|
import { convertImages } from "../lib/converter.js";
|
|
5
5
|
|
|
6
|
-
const CONFIG_PATH = path.resolve(
|
|
6
|
+
const CONFIG_PATH = path.resolve(
|
|
7
|
+
process.cwd(),
|
|
8
|
+
"image-converter.config.mjs"
|
|
9
|
+
);
|
|
7
10
|
|
|
8
11
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const configModule = await import(
|
|
13
|
+
pathToFileURL(CONFIG_PATH).href
|
|
14
|
+
);
|
|
15
|
+
const config = configModule.default;
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
const absDir = path.resolve(
|
|
18
|
+
process.cwd(),
|
|
19
|
+
config.source || config.dir || "."
|
|
20
|
+
);
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
await convertImages({
|
|
23
|
+
dir: absDir,
|
|
24
|
+
converted: config.converted ?? "*.{png,jpg,jpeg}",
|
|
25
|
+
format: config.format ?? "webp",
|
|
26
|
+
quality: config.quality ?? 80,
|
|
27
|
+
recursive: config.recursive ?? true,
|
|
28
|
+
removeOriginal: config.removeOriginal ?? false,
|
|
29
|
+
});
|
|
25
30
|
} catch (e) {
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
console.error(
|
|
32
|
+
"❌ Ошибка загрузки конфигурации:",
|
|
33
|
+
e.message
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
28
36
|
}
|
package/bin/watcher.mjs
CHANGED
package/lib/converter.js
CHANGED
|
@@ -9,64 +9,149 @@ const supportedExt = [".png", ".jpg", ".jpeg"];
|
|
|
9
9
|
|
|
10
10
|
const processingFiles = new Set();
|
|
11
11
|
|
|
12
|
-
async function safeUnlink(
|
|
13
|
-
|
|
12
|
+
async function safeUnlink(
|
|
13
|
+
file,
|
|
14
|
+
retries = 3,
|
|
15
|
+
delayMs = 200
|
|
16
|
+
) {
|
|
17
|
+
for (let i = 0; i < retries; i++) {
|
|
18
|
+
try {
|
|
19
|
+
await fs.unlink(file);
|
|
20
|
+
return;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (i === retries - 1) throw err;
|
|
23
|
+
await new Promise((res) =>
|
|
24
|
+
setTimeout(res, delayMs)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function getUniqueOutputPath(outFile, newBufferSize) {
|
|
14
31
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
const existingStat = await fs.stat(outFile);
|
|
33
|
+
|
|
34
|
+
// Если размеры совпадают - скорее всего это дубликат
|
|
35
|
+
if (existingStat.size === newBufferSize) {
|
|
36
|
+
return null; // Сигнал пропустить файл
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Размеры разные - нужно уникальное имя
|
|
40
|
+
const dir = path.dirname(outFile);
|
|
41
|
+
const ext = path.extname(outFile);
|
|
42
|
+
const nameWithoutExt = path.basename(outFile, ext);
|
|
43
|
+
|
|
44
|
+
let counter = 1;
|
|
45
|
+
let uniquePath;
|
|
46
|
+
|
|
47
|
+
while (true) {
|
|
48
|
+
uniquePath = path.join(
|
|
49
|
+
dir,
|
|
50
|
+
`${nameWithoutExt}(${counter})${ext}`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const stat = await fs.stat(uniquePath);
|
|
55
|
+
// Если файл с таким именем существует и размер совпадает - это дубликат
|
|
56
|
+
if (stat.size === newBufferSize) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
counter++;
|
|
60
|
+
} catch {
|
|
61
|
+
// Файл не существует - можем использовать это имя
|
|
62
|
+
return uniquePath;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Файл не существует - используем оригинальное имя
|
|
67
|
+
return outFile;
|
|
20
68
|
}
|
|
21
|
-
}
|
|
22
69
|
}
|
|
23
70
|
|
|
24
71
|
export async function convertImages({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
72
|
+
dir,
|
|
73
|
+
converted,
|
|
74
|
+
format,
|
|
75
|
+
quality,
|
|
76
|
+
recursive,
|
|
77
|
+
removeOriginal,
|
|
31
78
|
}) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
79
|
+
const rawPattern = recursive
|
|
80
|
+
? path.join(dir, "**", converted)
|
|
81
|
+
: path.join(dir, converted);
|
|
35
82
|
|
|
36
|
-
|
|
37
|
-
|
|
83
|
+
const pattern = rawPattern
|
|
84
|
+
.split(path.sep)
|
|
85
|
+
.join(path.posix.sep);
|
|
86
|
+
const files = await fg(pattern, {
|
|
87
|
+
caseSensitiveMatch: false,
|
|
88
|
+
});
|
|
38
89
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
if (processingFiles.has(file)) continue;
|
|
92
|
+
processingFiles.add(file);
|
|
42
93
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
try {
|
|
95
|
+
const ext = path.extname(file);
|
|
96
|
+
let outFile = file.replace(ext, `.${format}`);
|
|
97
|
+
|
|
98
|
+
// Игнорируем файлы, которые уже в целевом формате
|
|
99
|
+
if (
|
|
100
|
+
ext.toLowerCase() ===
|
|
101
|
+
`.${format.toLowerCase()}`
|
|
102
|
+
) {
|
|
103
|
+
processingFiles.delete(file);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const image = sharp(file);
|
|
108
|
+
const buffer =
|
|
109
|
+
format === "webp"
|
|
110
|
+
? await image
|
|
111
|
+
.webp({ quality })
|
|
112
|
+
.toBuffer()
|
|
113
|
+
: await image
|
|
114
|
+
.avif({ quality })
|
|
115
|
+
.toBuffer();
|
|
116
|
+
|
|
117
|
+
// Проверяем, не существует ли уже файл с таким именем
|
|
118
|
+
const finalPath = await getUniqueOutputPath(
|
|
119
|
+
outFile,
|
|
120
|
+
buffer.length
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (finalPath === null) {
|
|
124
|
+
// Файл с таким же размером уже существует - это дубликат
|
|
125
|
+
console.log(
|
|
126
|
+
`⊘ ${file} → пропущен (дубликат)`
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Удаляем оригинал-дубликат, если включена опция
|
|
130
|
+
if (removeOriginal) {
|
|
131
|
+
await safeUnlink(file);
|
|
132
|
+
console.log(`🗑️ ${file} → удален`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
processingFiles.delete(file);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
outFile = finalPath;
|
|
140
|
+
|
|
141
|
+
await fs.writeFile(outFile, buffer);
|
|
142
|
+
|
|
143
|
+
if (removeOriginal) {
|
|
144
|
+
await safeUnlink(file);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`✓ ${file} → ${outFile}`);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(
|
|
150
|
+
"Error covertation:",
|
|
151
|
+
err.message
|
|
152
|
+
);
|
|
153
|
+
} finally {
|
|
154
|
+
processingFiles.delete(file);
|
|
155
|
+
}
|
|
70
156
|
}
|
|
71
|
-
}
|
|
72
157
|
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
2
|
+
"name": "auto-image-converter",
|
|
3
|
+
"version": "2.1.2",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"image",
|
|
6
|
+
"converter",
|
|
7
|
+
"webp",
|
|
8
|
+
"avif",
|
|
9
|
+
"sharp",
|
|
10
|
+
"watch",
|
|
11
|
+
"optimization",
|
|
12
|
+
"png",
|
|
13
|
+
"jpg",
|
|
14
|
+
"jpeg",
|
|
15
|
+
"for frontend"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"auto-convert-images": "./bin/index.js",
|
|
20
|
+
"auto-convert-images-watch": "./bin/watcher.mjs"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chokidar": "^4.0.3",
|
|
24
|
+
"commander": "^14.0.1",
|
|
25
|
+
"fast-glob": "^3.3.3",
|
|
26
|
+
"sharp": "^0.34.4"
|
|
27
|
+
}
|
|
28
28
|
}
|