metalsmith-optimize-images 0.9.0 → 0.9.3
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 +152 -22
- package/lib/index.cjs +389 -10
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +387 -8
- package/lib/index.js.map +1 -1
- package/package.json +8 -7
package/lib/index.cjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var path = require('path');
|
|
4
|
-
var mkdirp = require('mkdirp');
|
|
5
|
-
var cheerio = require('cheerio');
|
|
6
4
|
var fs = require('fs');
|
|
5
|
+
var mkdirp = require('mkdirp');
|
|
7
6
|
var sharp = require('sharp');
|
|
7
|
+
var cheerio = require('cheerio');
|
|
8
8
|
var crypto = require('crypto');
|
|
9
9
|
|
|
10
10
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
@@ -28,10 +28,10 @@ function _interopNamespace(e) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
31
|
-
var mkdirp__namespace = /*#__PURE__*/_interopNamespace(mkdirp);
|
|
32
|
-
var cheerio__namespace = /*#__PURE__*/_interopNamespace(cheerio);
|
|
33
31
|
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
32
|
+
var mkdirp__namespace = /*#__PURE__*/_interopNamespace(mkdirp);
|
|
34
33
|
var sharp__default = /*#__PURE__*/_interopDefaultLegacy(sharp);
|
|
34
|
+
var cheerio__namespace = /*#__PURE__*/_interopNamespace(cheerio);
|
|
35
35
|
var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -135,7 +135,11 @@ function buildConfig(options = {}) {
|
|
|
135
135
|
width: 50,
|
|
136
136
|
quality: 30,
|
|
137
137
|
blur: 10
|
|
138
|
-
}
|
|
138
|
+
},
|
|
139
|
+
// Background image processing settings
|
|
140
|
+
processUnusedImages: true,
|
|
141
|
+
// Process images not found in HTML for background use
|
|
142
|
+
imagePattern: '**/*.{jpg,jpeg,png,gif,webp,avif}' // Pattern to find images for background processing
|
|
139
143
|
};
|
|
140
144
|
|
|
141
145
|
// Special handling for formatOptions to ensure deep merging
|
|
@@ -176,9 +180,9 @@ function buildConfig(options = {}) {
|
|
|
176
180
|
* @return {string} - A short hash string (8 characters)
|
|
177
181
|
*/
|
|
178
182
|
function generateHash(buffer) {
|
|
179
|
-
//
|
|
183
|
+
// SHA-256 for cache-busting - using secure algorithm to satisfy security scanners
|
|
180
184
|
// Only use first 8 characters to keep filenames manageable
|
|
181
|
-
return crypto__default["default"].createHash('
|
|
185
|
+
return crypto__default["default"].createHash('sha256').update(buffer).digest('hex').slice(0, 8);
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
/**
|
|
@@ -268,7 +272,17 @@ async function processImageToVariants(buffer, originalPath, debugFn, config) {
|
|
|
268
272
|
} else {
|
|
269
273
|
// For specific formats (avif, webp, etc.), apply format-specific options
|
|
270
274
|
const formatOptions = config.formatOptions[format] || {};
|
|
271
|
-
|
|
275
|
+
if (format === 'avif') {
|
|
276
|
+
formatted = resized.clone().avif(formatOptions);
|
|
277
|
+
} else if (format === 'webp') {
|
|
278
|
+
formatted = resized.clone().webp(formatOptions);
|
|
279
|
+
} else if (format === 'jpeg') {
|
|
280
|
+
formatted = resized.clone().jpeg(formatOptions);
|
|
281
|
+
} else if (format === 'png') {
|
|
282
|
+
formatted = resized.clone().png(formatOptions);
|
|
283
|
+
} else {
|
|
284
|
+
formatted = resized.clone()[format](formatOptions);
|
|
285
|
+
}
|
|
272
286
|
}
|
|
273
287
|
|
|
274
288
|
// Generate the actual image buffer - this is where compression happens
|
|
@@ -936,9 +950,12 @@ async function processHtmlFile(htmlFile, fileData, files, metalsmith, processedI
|
|
|
936
950
|
*/
|
|
937
951
|
function generateMetadata(processedImages, files, config) {
|
|
938
952
|
const metadataObj = {};
|
|
939
|
-
processedImages.forEach((
|
|
953
|
+
processedImages.forEach((value, key) => {
|
|
940
954
|
// Extract the original path from the cache key (path:mtime)
|
|
941
955
|
const [path] = key.split(':');
|
|
956
|
+
|
|
957
|
+
// Handle both array format (from background processing) and object format (from HTML processing)
|
|
958
|
+
const variants = Array.isArray(value) ? value : value.variants;
|
|
942
959
|
metadataObj[path] = variants.map(v => ({
|
|
943
960
|
path: v.path,
|
|
944
961
|
width: v.width,
|
|
@@ -1124,6 +1141,9 @@ function injectProgressiveAssets($) {
|
|
|
1124
1141
|
* @param {number} [options.placeholder.width] - Placeholder image width (default: 50)
|
|
1125
1142
|
* @param {number} [options.placeholder.quality] - Placeholder image quality (default: 30)
|
|
1126
1143
|
* @param {number} [options.placeholder.blur] - Placeholder image blur amount (default: 10)
|
|
1144
|
+
* @param {boolean} [options.processUnusedImages] - Whether to process unused images for background use (default: true)
|
|
1145
|
+
* @param {string} [options.imagePattern] - Glob pattern to find images for background processing (default: `**\/*.{jpg,jpeg,png,gif,webp,avif}`)
|
|
1146
|
+
* @param {string} [options.imageFolder] - Folder to scan for background images, relative to source (default: 'lib/assets/images')
|
|
1127
1147
|
* @return {Function} - Metalsmith plugin function
|
|
1128
1148
|
*/
|
|
1129
1149
|
function optimizeImagesPlugin(options = {}) {
|
|
@@ -1149,7 +1169,19 @@ function optimizeImagesPlugin(options = {}) {
|
|
|
1149
1169
|
mkdirp__namespace.mkdirpSync(outputPath);
|
|
1150
1170
|
|
|
1151
1171
|
// Find all HTML files that match the pattern (default: **/*.html)
|
|
1152
|
-
|
|
1172
|
+
// Also ensure they actually end with .html to avoid processing CSS/JS files
|
|
1173
|
+
const htmlFiles = Object.keys(files).filter(file => {
|
|
1174
|
+
// Must match the HTML pattern
|
|
1175
|
+
if (!metalsmith.match(config.htmlPattern, file)) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Must actually be an HTML file
|
|
1180
|
+
if (!file.endsWith('.html')) {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
return true;
|
|
1184
|
+
});
|
|
1153
1185
|
if (htmlFiles.length === 0) {
|
|
1154
1186
|
debug('No HTML files found');
|
|
1155
1187
|
return done();
|
|
@@ -1176,6 +1208,13 @@ function optimizeImagesPlugin(options = {}) {
|
|
|
1176
1208
|
}));
|
|
1177
1209
|
}));
|
|
1178
1210
|
|
|
1211
|
+
// Process unused images for background image support
|
|
1212
|
+
// This finds images that weren't processed during HTML scanning and creates variants
|
|
1213
|
+
// for use in CSS background-image with image-set()
|
|
1214
|
+
if (config.processUnusedImages) {
|
|
1215
|
+
await processUnusedImages(files, metalsmith, processedImages, debug, config);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1179
1218
|
// Optional: Generate a JSON metadata file with information about all processed images
|
|
1180
1219
|
// Useful for debugging or integration with other tools
|
|
1181
1220
|
if (config.generateMetadata) {
|
|
@@ -1191,5 +1230,345 @@ function optimizeImagesPlugin(options = {}) {
|
|
|
1191
1230
|
};
|
|
1192
1231
|
}
|
|
1193
1232
|
|
|
1233
|
+
/**
|
|
1234
|
+
* Process unused images for background image support
|
|
1235
|
+
* Finds images that weren't processed during HTML scanning and creates 1x/2x variants
|
|
1236
|
+
* for use in CSS background-image with image-set()
|
|
1237
|
+
* @param {Object} files - Metalsmith files object
|
|
1238
|
+
* @param {Object} metalsmith - Metalsmith instance
|
|
1239
|
+
* @param {Map} processedImages - Cache of already processed images
|
|
1240
|
+
* @param {Function} debug - Debug function
|
|
1241
|
+
* @param {Object} config - Plugin configuration
|
|
1242
|
+
* @return {Promise<void>} - Promise that resolves when processing is complete
|
|
1243
|
+
*/
|
|
1244
|
+
async function processUnusedImages(files, metalsmith, processedImages, debug, config) {
|
|
1245
|
+
debug('Processing unused images for background image support');
|
|
1246
|
+
|
|
1247
|
+
// Get all image paths that were already processed during HTML scanning
|
|
1248
|
+
const processedImagePaths = new Set();
|
|
1249
|
+
processedImages.forEach((_variants, cacheKey) => {
|
|
1250
|
+
const [imagePath] = cacheKey.split(':');
|
|
1251
|
+
processedImagePaths.add(imagePath);
|
|
1252
|
+
});
|
|
1253
|
+
debug(`Processed image paths from HTML: ${Array.from(processedImagePaths).join(', ')}`);
|
|
1254
|
+
|
|
1255
|
+
// Find images that weren't processed during HTML scanning using hybrid approach
|
|
1256
|
+
const allBackgroundImages = await findUnprocessedImages(files, metalsmith, config, processedImagePaths, debug);
|
|
1257
|
+
debug(`Background images found to process: ${allBackgroundImages.map(img => img.path).join(', ')}`);
|
|
1258
|
+
if (allBackgroundImages.length === 0) {
|
|
1259
|
+
debug('No unused images found to process');
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
debug(`Found ${allBackgroundImages.length} unused images to process for background use`);
|
|
1263
|
+
|
|
1264
|
+
// Process background images in parallel for better performance
|
|
1265
|
+
await Promise.all(allBackgroundImages.map(async imageObj => {
|
|
1266
|
+
try {
|
|
1267
|
+
debug(`Processing background image: ${imageObj.path} (source: ${imageObj.source})`);
|
|
1268
|
+
|
|
1269
|
+
// Generate background variants with original size and half size
|
|
1270
|
+
const variants = await processBackgroundImageVariants(imageObj.buffer, imageObj.path, debug, config);
|
|
1271
|
+
|
|
1272
|
+
// Save all generated variants to Metalsmith files object
|
|
1273
|
+
variants.forEach(variant => {
|
|
1274
|
+
files[variant.path] = {
|
|
1275
|
+
contents: variant.buffer
|
|
1276
|
+
};
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
// Cache the variants (using current timestamp as mtime for unused images)
|
|
1280
|
+
const cacheKey = `${imageObj.path}:${Date.now()}`;
|
|
1281
|
+
processedImages.set(cacheKey, variants);
|
|
1282
|
+
debug(`Generated ${variants.length} background variants for ${imageObj.path}`);
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
debug(`Error processing background image ${imageObj.path}: ${err.message}`);
|
|
1285
|
+
}
|
|
1286
|
+
}));
|
|
1287
|
+
debug('Background image processing complete');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Find images that weren't processed during HTML scanning
|
|
1292
|
+
* Uses a hybrid approach: scans filesystem first, then falls back to Metalsmith files object
|
|
1293
|
+
* @param {Object} files - Metalsmith files object
|
|
1294
|
+
* @param {Object} metalsmith - Metalsmith instance
|
|
1295
|
+
* @param {Object} config - Plugin configuration
|
|
1296
|
+
* @param {Set} processedImagePaths - Set of already processed image paths
|
|
1297
|
+
* @param {Function} debug - Debug function
|
|
1298
|
+
* @return {Promise<Array>} - Array of unprocessed image objects with {path, buffer}
|
|
1299
|
+
*/
|
|
1300
|
+
async function findUnprocessedImages(files, metalsmith, config, processedImagePaths, debug) {
|
|
1301
|
+
const unprocessedImages = [];
|
|
1302
|
+
const sourceImagesDir = path__default["default"].join(metalsmith.source(), 'lib/assets/images');
|
|
1303
|
+
debug(`Looking for unprocessed images using hybrid approach`);
|
|
1304
|
+
|
|
1305
|
+
// Method 1: Scan filesystem (for real testbed scenario)
|
|
1306
|
+
try {
|
|
1307
|
+
debug(`Attempting to scan source directory: ${sourceImagesDir}`);
|
|
1308
|
+
debug(`Source directory exists: ${fs__default["default"].existsSync(sourceImagesDir)}`);
|
|
1309
|
+
debug(`Metalsmith source: ${metalsmith.source()}`);
|
|
1310
|
+
debug(`Metalsmith destination: ${metalsmith.destination()}`);
|
|
1311
|
+
if (fs__default["default"].existsSync(sourceImagesDir)) {
|
|
1312
|
+
debug(`Scanning source directory: ${sourceImagesDir}`);
|
|
1313
|
+
const scanDirectory = (dir, relativePath = '') => {
|
|
1314
|
+
const items = fs__default["default"].readdirSync(dir);
|
|
1315
|
+
debug(`Found ${items.length} items in ${dir}`);
|
|
1316
|
+
for (const item of items) {
|
|
1317
|
+
if (item === '.DS_Store') {
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const fullPath = path__default["default"].join(dir, item);
|
|
1321
|
+
const itemRelativePath = path__default["default"].join(relativePath, item);
|
|
1322
|
+
if (fs__default["default"].statSync(fullPath).isDirectory()) {
|
|
1323
|
+
debug(`Scanning subdirectory: ${item}`);
|
|
1324
|
+
scanDirectory(fullPath, itemRelativePath);
|
|
1325
|
+
} else {
|
|
1326
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif'];
|
|
1327
|
+
if (imageExtensions.some(ext => item.toLowerCase().endsWith(ext))) {
|
|
1328
|
+
// Skip if this is in the responsive output directory
|
|
1329
|
+
if (itemRelativePath.startsWith('responsive/') || itemRelativePath.includes('/responsive/') || fullPath.includes(config.outputDir)) {
|
|
1330
|
+
debug(`Skipping responsive variant: ${itemRelativePath}`);
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
const buildPath = path__default["default"].join('assets/images', itemRelativePath);
|
|
1334
|
+
const normalizedBuildPath = buildPath.replace(/\\/g, '/');
|
|
1335
|
+
debug(`Found filesystem image: ${item} -> ${normalizedBuildPath}`);
|
|
1336
|
+
debug(`Already processed? ${processedImagePaths.has(normalizedBuildPath)}`);
|
|
1337
|
+
if (!processedImagePaths.has(normalizedBuildPath)) {
|
|
1338
|
+
debug(`Found unprocessed filesystem image: ${itemRelativePath}`);
|
|
1339
|
+
const imageBuffer = fs__default["default"].readFileSync(fullPath);
|
|
1340
|
+
unprocessedImages.push({
|
|
1341
|
+
path: itemRelativePath,
|
|
1342
|
+
buffer: imageBuffer,
|
|
1343
|
+
source: 'filesystem'
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
scanDirectory(sourceImagesDir);
|
|
1351
|
+
} else {
|
|
1352
|
+
debug(`Source directory does not exist, trying alternative paths...`);
|
|
1353
|
+
|
|
1354
|
+
// Try alternative paths
|
|
1355
|
+
const altPaths = [path__default["default"].join(metalsmith.source(), 'assets/images'), path__default["default"].join(metalsmith.source(), 'images'), path__default["default"].join(metalsmith.destination(), 'assets/images'), path__default["default"].join(process.cwd(), 'lib/assets/images'), path__default["default"].join(process.cwd(), 'src/assets/images')];
|
|
1356
|
+
for (const altPath of altPaths) {
|
|
1357
|
+
debug(`Trying alternative path: ${altPath} - exists: ${fs__default["default"].existsSync(altPath)}`);
|
|
1358
|
+
if (fs__default["default"].existsSync(altPath)) {
|
|
1359
|
+
debug(`Found images at alternative path: ${altPath}`);
|
|
1360
|
+
|
|
1361
|
+
// Scan the found alternative path
|
|
1362
|
+
const scanAlternativeDirectory = (dir, relativePath = '') => {
|
|
1363
|
+
const items = fs__default["default"].readdirSync(dir);
|
|
1364
|
+
debug(`Found ${items.length} items in alternative path ${dir}`);
|
|
1365
|
+
for (const item of items) {
|
|
1366
|
+
if (item === '.DS_Store') {
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const fullPath = path__default["default"].join(dir, item);
|
|
1370
|
+
const itemRelativePath = path__default["default"].join(relativePath, item);
|
|
1371
|
+
if (fs__default["default"].statSync(fullPath).isDirectory()) {
|
|
1372
|
+
debug(`Scanning alternative subdirectory: ${item}`);
|
|
1373
|
+
scanAlternativeDirectory(fullPath, itemRelativePath);
|
|
1374
|
+
} else {
|
|
1375
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif'];
|
|
1376
|
+
if (imageExtensions.some(ext => item.toLowerCase().endsWith(ext))) {
|
|
1377
|
+
// Skip if this is in the responsive output directory
|
|
1378
|
+
if (itemRelativePath.startsWith('responsive/') || itemRelativePath.includes('/responsive/') || fullPath.includes(config.outputDir)) {
|
|
1379
|
+
debug(`Skipping responsive variant in alt scan: ${itemRelativePath}`);
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// For build directory, the path structure is already correct
|
|
1384
|
+
const buildPath = altPath.includes('build') ? path__default["default"].join('assets/images', itemRelativePath) : path__default["default"].join('assets/images', itemRelativePath);
|
|
1385
|
+
const normalizedBuildPath = buildPath.replace(/\\/g, '/');
|
|
1386
|
+
debug(`Found alternative filesystem image: ${item} -> ${normalizedBuildPath}`);
|
|
1387
|
+
debug(`Already processed? ${processedImagePaths.has(normalizedBuildPath)}`);
|
|
1388
|
+
if (!processedImagePaths.has(normalizedBuildPath)) {
|
|
1389
|
+
debug(`Found unprocessed alternative filesystem image: ${itemRelativePath}`);
|
|
1390
|
+
const imageBuffer = fs__default["default"].readFileSync(fullPath);
|
|
1391
|
+
unprocessedImages.push({
|
|
1392
|
+
path: itemRelativePath,
|
|
1393
|
+
buffer: imageBuffer,
|
|
1394
|
+
source: 'filesystem-alt'
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
scanAlternativeDirectory(altPath);
|
|
1402
|
+
break; // Stop after finding and scanning the first valid path
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
debug(`Error scanning filesystem: ${err.message}`);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Method 2: Scan Metalsmith files object (for test scenarios and edge cases)
|
|
1411
|
+
debug(`Scanning Metalsmith files object`);
|
|
1412
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif'];
|
|
1413
|
+
Object.keys(files).forEach(filePath => {
|
|
1414
|
+
// Skip if not an image
|
|
1415
|
+
if (!imageExtensions.some(ext => filePath.toLowerCase().endsWith(ext))) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Skip if it's already a responsive variant (comprehensive checks)
|
|
1420
|
+
if (filePath.startsWith(`${config.outputDir}/`) || filePath.includes('/responsive/') || filePath.includes('responsive-images-manifest.json') || filePath.match(/-\d+w(-[a-f0-9]+)?\.(avif|webp|jpg|jpeg|png)$/i)) {
|
|
1421
|
+
debug(`Skipping responsive variant in files object: ${filePath}`);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Skip if already processed during HTML scanning
|
|
1426
|
+
if (processedImagePaths.has(filePath)) {
|
|
1427
|
+
debug(`Skipping already processed files object image: ${filePath}`);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Check if we already found this image from filesystem scan
|
|
1432
|
+
const isAlreadyFound = unprocessedImages.some(img => {
|
|
1433
|
+
// For files object images starting with 'images/', check if filesystem found the same file
|
|
1434
|
+
if (filePath.startsWith('images/')) {
|
|
1435
|
+
const relativePath = filePath.replace('images/', '');
|
|
1436
|
+
return img.path === relativePath;
|
|
1437
|
+
}
|
|
1438
|
+
return false;
|
|
1439
|
+
});
|
|
1440
|
+
if (!isAlreadyFound) {
|
|
1441
|
+
debug(`Found unprocessed files object image: ${filePath}`);
|
|
1442
|
+
unprocessedImages.push({
|
|
1443
|
+
path: filePath,
|
|
1444
|
+
buffer: files[filePath].contents,
|
|
1445
|
+
source: 'files'
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
debug(`Found ${unprocessedImages.length} unprocessed images total`);
|
|
1450
|
+
return unprocessedImages;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Process a background image to create 1x (original) and 2x (half-size) variants
|
|
1455
|
+
* for use with CSS image-set() for retina displays
|
|
1456
|
+
* @param {Buffer} buffer - Original image buffer
|
|
1457
|
+
* @param {string} originalPath - Original image path
|
|
1458
|
+
* @param {Function} debugFn - Debug function for logging
|
|
1459
|
+
* @param {Object} config - Plugin configuration
|
|
1460
|
+
* @return {Promise<Array<Object>>} - Array of generated variants
|
|
1461
|
+
*/
|
|
1462
|
+
async function processBackgroundImageVariants(buffer, originalPath, debugFn, config) {
|
|
1463
|
+
const image = sharp__default["default"](buffer);
|
|
1464
|
+
const metadata = await image.metadata();
|
|
1465
|
+
const variants = [];
|
|
1466
|
+
debugFn(`Processing background image ${originalPath}: ${metadata.width}x${metadata.height}`);
|
|
1467
|
+
|
|
1468
|
+
// Create 1x (original size) and 2x (half size) variants
|
|
1469
|
+
const sizes = [{
|
|
1470
|
+
width: metadata.width,
|
|
1471
|
+
density: '1x'
|
|
1472
|
+
}, {
|
|
1473
|
+
width: Math.round(metadata.width / 2),
|
|
1474
|
+
density: '2x'
|
|
1475
|
+
}];
|
|
1476
|
+
|
|
1477
|
+
// Process both sizes in parallel
|
|
1478
|
+
const sizePromises = sizes.map(async size => {
|
|
1479
|
+
// Create a Sharp instance for this size
|
|
1480
|
+
const resized = image.clone().resize({
|
|
1481
|
+
width: size.width,
|
|
1482
|
+
withoutEnlargement: true // Don't upscale images
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
// Get actual dimensions after resize
|
|
1486
|
+
const resizedMeta = await resized.metadata();
|
|
1487
|
+
|
|
1488
|
+
// Process each format in parallel for this size
|
|
1489
|
+
const formatPromises = config.formats.map(async format => {
|
|
1490
|
+
try {
|
|
1491
|
+
// Skip problematic format combinations
|
|
1492
|
+
if (format === 'original' && metadata.format.toLowerCase() === 'webp') {
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Determine output format and Sharp method
|
|
1497
|
+
let outputFormat = format;
|
|
1498
|
+
let sharpMethod = format;
|
|
1499
|
+
if (format === 'original') {
|
|
1500
|
+
outputFormat = metadata.format.toLowerCase();
|
|
1501
|
+
sharpMethod = outputFormat === 'jpeg' ? 'jpeg' : outputFormat;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Apply format-specific processing
|
|
1505
|
+
let processedImage = resized.clone();
|
|
1506
|
+
const formatOptions = config.formatOptions[format === 'original' ? outputFormat : format] || {};
|
|
1507
|
+
if (sharpMethod === 'avif') {
|
|
1508
|
+
processedImage = processedImage.avif(formatOptions);
|
|
1509
|
+
} else if (sharpMethod === 'webp') {
|
|
1510
|
+
processedImage = processedImage.webp(formatOptions);
|
|
1511
|
+
} else if (sharpMethod === 'jpeg') {
|
|
1512
|
+
processedImage = processedImage.jpeg(formatOptions);
|
|
1513
|
+
} else if (sharpMethod === 'png') {
|
|
1514
|
+
processedImage = processedImage.png(formatOptions);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Generate output buffer
|
|
1518
|
+
const outputBuffer = await processedImage.toBuffer();
|
|
1519
|
+
|
|
1520
|
+
// Generate variant path without hash for easier CSS usage
|
|
1521
|
+
const variantPath = generateBackgroundVariantPath(originalPath, size.width, outputFormat, config);
|
|
1522
|
+
debugFn(`Generated background variant: ${variantPath} (${size.density})`);
|
|
1523
|
+
return {
|
|
1524
|
+
path: variantPath,
|
|
1525
|
+
buffer: outputBuffer,
|
|
1526
|
+
width: resizedMeta.width,
|
|
1527
|
+
height: resizedMeta.height,
|
|
1528
|
+
format: outputFormat,
|
|
1529
|
+
density: size.density
|
|
1530
|
+
};
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
debugFn(`Error processing ${format} format for ${originalPath}: ${err.message}`);
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
const formatResults = await Promise.all(formatPromises);
|
|
1537
|
+
return formatResults.filter(result => result !== null);
|
|
1538
|
+
});
|
|
1539
|
+
const sizeResults = await Promise.all(sizePromises);
|
|
1540
|
+
|
|
1541
|
+
// Flatten the results
|
|
1542
|
+
sizeResults.forEach(formatVariants => {
|
|
1543
|
+
variants.push(...formatVariants);
|
|
1544
|
+
});
|
|
1545
|
+
debugFn(`Generated ${variants.length} background variants for ${originalPath}`);
|
|
1546
|
+
return variants;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Generate background image variant path without hash for easier CSS usage
|
|
1551
|
+
* Creates predictable filenames that can be written in CSS without knowing the hash
|
|
1552
|
+
* @param {string} originalPath - Original image path
|
|
1553
|
+
* @param {number} width - Target width
|
|
1554
|
+
* @param {string} format - Target format
|
|
1555
|
+
* @param {Object} config - Plugin configuration
|
|
1556
|
+
* @return {string} - Generated path without hash
|
|
1557
|
+
*/
|
|
1558
|
+
function generateBackgroundVariantPath(originalPath, width, format, config) {
|
|
1559
|
+
const parsedPath = path__default["default"].parse(originalPath);
|
|
1560
|
+
const originalFormat = parsedPath.ext.slice(1).toLowerCase();
|
|
1561
|
+
|
|
1562
|
+
// If format is 'original', use the source format
|
|
1563
|
+
const outputFormat = format === 'original' ? originalFormat : format;
|
|
1564
|
+
|
|
1565
|
+
// Create background pattern without hash: '[filename]-[width]w.[format]'
|
|
1566
|
+
// Results in: 'header1-1000w.webp' instead of 'header1-1000w-abc12345.webp'
|
|
1567
|
+
const outputName = config.outputPattern.replace('[filename]', parsedPath.name).replace('[width]', width).replace('[format]', outputFormat).replace('-[hash]', '') // Remove hash placeholder and preceding dash
|
|
1568
|
+
.replace('[hash]', ''); // Remove any remaining hash placeholder
|
|
1569
|
+
|
|
1570
|
+
return path__default["default"].join(config.outputDir, outputName);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1194
1573
|
module.exports = optimizeImagesPlugin;
|
|
1195
1574
|
//# sourceMappingURL=index.cjs.map
|