-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompress-images.js
More file actions
155 lines (127 loc) · 4.13 KB
/
compress-images.js
File metadata and controls
155 lines (127 loc) · 4.13 KB
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import sharp from "sharp";
import { readdir, stat } from "fs/promises";
import { join, extname } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const blogDir = join(__dirname, "..", "src", "content", "blog");
// Compression settings
const PNG_QUALITY = 80; // 1-100, lower = smaller file
const PNG_COMPRESSION = 9; // 0-9, higher = more compression (slower)
const MIN_SAVINGS_PERCENT = 5; // Only replace if we save at least this much
async function findImages(dir) {
const images = [];
async function scan(currentDir) {
const entries = await readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
await scan(fullPath);
} else if (entry.isFile()) {
const ext = extname(entry.name).toLowerCase();
if ([".png", ".jpg", ".jpeg"].includes(ext)) {
images.push(fullPath);
}
}
}
}
await scan(dir);
return images;
}
async function compressImage(imagePath) {
const ext = extname(imagePath).toLowerCase();
const originalStats = await stat(imagePath);
const originalSize = originalStats.size;
let pipeline = sharp(imagePath);
const metadata = await pipeline.metadata();
// Skip if already very small
if (originalSize < 10000) {
return { skipped: true, reason: "already small" };
}
let outputBuffer;
if (ext === ".png") {
// For PNGs: use palette-based compression when possible
outputBuffer = await sharp(imagePath)
.png({
compressionLevel: PNG_COMPRESSION,
palette: true,
quality: PNG_QUALITY,
effort: 10, // max effort for smallest size
})
.toBuffer();
} else if (ext === ".jpg" || ext === ".jpeg") {
outputBuffer = await sharp(imagePath)
.jpeg({
quality: 85,
mozjpeg: true,
})
.toBuffer();
}
const newSize = outputBuffer.length;
const savingsPercent = ((originalSize - newSize) / originalSize) * 100;
if (savingsPercent >= MIN_SAVINGS_PERCENT) {
await sharp(outputBuffer).toFile(imagePath);
return {
compressed: true,
originalSize,
newSize,
savingsPercent: savingsPercent.toFixed(1),
};
}
return {
skipped: true,
reason: `savings too small (${savingsPercent.toFixed(1)}%)`,
};
}
function formatBytes(bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
}
async function main() {
const args = process.argv.slice(2);
let targetDir = blogDir;
// Allow passing a specific directory
if (args[0]) {
targetDir = args[0];
}
console.log(`\nScanning for images in: ${targetDir}\n`);
const images = await findImages(targetDir);
console.log(`Found ${images.length} images\n`);
let totalOriginal = 0;
let totalNew = 0;
let compressedCount = 0;
let skippedCount = 0;
for (const imagePath of images) {
const relativePath = imagePath.replace(blogDir, "").replace(/^[/\\]/, "");
process.stdout.write(`Processing: ${relativePath}... `);
try {
const result = await compressImage(imagePath);
if (result.compressed) {
console.log(
`✓ ${formatBytes(result.originalSize)} → ${formatBytes(result.newSize)} (-${result.savingsPercent}%)`
);
totalOriginal += result.originalSize;
totalNew += result.newSize;
compressedCount++;
} else {
console.log(`⊘ skipped (${result.reason})`);
skippedCount++;
}
} catch (err) {
console.log(`✗ error: ${err.message}`);
}
}
console.log(`\n${"─".repeat(60)}`);
console.log(`Compressed: ${compressedCount} images`);
console.log(`Skipped: ${skippedCount} images`);
if (compressedCount > 0) {
const totalSavings = totalOriginal - totalNew;
const totalPercent = ((totalSavings / totalOriginal) * 100).toFixed(1);
console.log(
`Total savings: ${formatBytes(totalSavings)} (${totalPercent}%)`
);
}
console.log();
}
main().catch(console.error);